mirror of
https://github.com/FoundationAgents/MetaGPT.git
synced 2026-07-05 16:02:14 +02:00
Merge branch 'main' into feature-openai-v1
This commit is contained in:
commit
9a4f0d555c
260 changed files with 10576 additions and 3191 deletions
|
|
@ -9,11 +9,10 @@ from enum import Enum
|
|||
|
||||
from metagpt.actions.action import Action
|
||||
from metagpt.actions.action_output import ActionOutput
|
||||
from metagpt.actions.add_requirement import BossRequirement
|
||||
from metagpt.actions.add_requirement import UserRequirement
|
||||
from metagpt.actions.debug_error import DebugError
|
||||
from metagpt.actions.design_api import WriteDesign
|
||||
from metagpt.actions.design_api_review import DesignReview
|
||||
from metagpt.actions.design_filenames import DesignFilenames
|
||||
from metagpt.actions.project_management import AssignTasks, WriteTasks
|
||||
from metagpt.actions.research import CollectLinks, WebBrowseAndSummarize, ConductResearch
|
||||
from metagpt.actions.run_code import RunCode
|
||||
|
|
@ -28,12 +27,11 @@ from metagpt.actions.write_test import WriteTest
|
|||
class ActionType(Enum):
|
||||
"""All types of Actions, used for indexing."""
|
||||
|
||||
ADD_REQUIREMENT = BossRequirement
|
||||
ADD_REQUIREMENT = UserRequirement
|
||||
WRITE_PRD = WritePRD
|
||||
WRITE_PRD_REVIEW = WritePRDReview
|
||||
WRITE_DESIGN = WriteDesign
|
||||
DESIGN_REVIEW = DesignReview
|
||||
DESIGN_FILENAMES = DesignFilenames
|
||||
WRTIE_CODE = WriteCode
|
||||
WRITE_CODE_REVIEW = WriteCodeReview
|
||||
WRITE_TEST = WriteTest
|
||||
|
|
|
|||
|
|
@ -5,36 +5,60 @@
|
|||
@Author : alexanderwu
|
||||
@File : action.py
|
||||
"""
|
||||
import re
|
||||
from abc import ABC
|
||||
from typing import Optional
|
||||
|
||||
from tenacity import retry, stop_after_attempt, wait_fixed
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from metagpt.actions.action_output import ActionOutput
|
||||
from metagpt.llm import LLM
|
||||
from metagpt.logs import logger
|
||||
from metagpt.utils.common import OutputParser
|
||||
from metagpt.utils.custom_decoder import CustomDecoder
|
||||
from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
from metagpt.schema import (
|
||||
CodeSummarizeContext,
|
||||
CodingContext,
|
||||
RunCodeContext,
|
||||
TestingContext,
|
||||
)
|
||||
|
||||
action_subclass_registry = {}
|
||||
|
||||
|
||||
class Action(ABC):
|
||||
def __init__(self, name: str = "", context=None, llm: LLM = None):
|
||||
self.name: str = name
|
||||
if llm is None:
|
||||
llm = LLM()
|
||||
self.llm = llm
|
||||
self.context = context
|
||||
self.prefix = ""
|
||||
self.profile = ""
|
||||
self.desc = ""
|
||||
self.content = ""
|
||||
self.instruct_content = None
|
||||
class Action(BaseModel):
|
||||
name: str = ""
|
||||
llm: BaseGPTAPI = Field(default_factory=LLM, exclude=True)
|
||||
context: Union[dict, CodingContext, CodeSummarizeContext, TestingContext, RunCodeContext, str, None] = ""
|
||||
prefix = "" # aask*时会加上prefix,作为system_message
|
||||
desc = "" # for skill manager
|
||||
# node: ActionNode = Field(default_factory=ActionNode, exclude=True)
|
||||
|
||||
def set_prefix(self, prefix, profile):
|
||||
# builtin variables
|
||||
builtin_class_name: str = ""
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def __init__(self, **kwargs: Any):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# deserialize child classes dynamically for inherited `action`
|
||||
object.__setattr__(self, "builtin_class_name", self.__class__.__name__)
|
||||
self.__fields__["builtin_class_name"].default = self.__class__.__name__
|
||||
|
||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||
super().__init_subclass__(**kwargs)
|
||||
action_subclass_registry[cls.__name__] = cls
|
||||
|
||||
def dict(self, *args, **kwargs) -> "DictStrAny":
|
||||
obj_dict = super(Action, self).dict(*args, **kwargs)
|
||||
if "llm" in obj_dict:
|
||||
obj_dict.pop("llm")
|
||||
return obj_dict
|
||||
|
||||
def set_prefix(self, prefix):
|
||||
"""Set prefix for later usage"""
|
||||
self.prefix = prefix
|
||||
self.profile = profile
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
return self.__class__.__name__
|
||||
|
|
@ -49,41 +73,6 @@ class Action(ABC):
|
|||
system_msgs.append(self.prefix)
|
||||
return await self.llm.aask(prompt, system_msgs)
|
||||
|
||||
@retry(stop=stop_after_attempt(3), wait=wait_fixed(1))
|
||||
async def _aask_v1(
|
||||
self,
|
||||
prompt: str,
|
||||
output_class_name: str,
|
||||
output_data_mapping: dict,
|
||||
system_msgs: Optional[list[str]] = None,
|
||||
format="markdown", # compatible to original format
|
||||
) -> ActionOutput:
|
||||
"""Append default prefix"""
|
||||
if not system_msgs:
|
||||
system_msgs = []
|
||||
system_msgs.append(self.prefix)
|
||||
content = await self.llm.aask(prompt, system_msgs)
|
||||
logger.debug(content)
|
||||
output_class = ActionOutput.create_model_class(output_class_name, output_data_mapping)
|
||||
|
||||
if format == "json":
|
||||
pattern = r"\[CONTENT\](\s*\{.*?\}\s*)\[/CONTENT\]"
|
||||
matches = re.findall(pattern, content, re.DOTALL)
|
||||
|
||||
for match in matches:
|
||||
if match:
|
||||
content = match
|
||||
break
|
||||
|
||||
parsed_data = CustomDecoder(strict=False).decode(content)
|
||||
|
||||
else: # using markdown parser
|
||||
parsed_data = OutputParser.parse_data_with_mapping(content, output_data_mapping)
|
||||
|
||||
logger.debug(parsed_data)
|
||||
instruct_content = output_class(**parsed_data)
|
||||
return ActionOutput(content, instruct_content)
|
||||
|
||||
async def run(self, *args, **kwargs):
|
||||
"""Run action"""
|
||||
raise NotImplementedError("The run method should be implemented in a subclass.")
|
||||
|
|
|
|||
337
metagpt/actions/action_node.py
Normal file
337
metagpt/actions/action_node.py
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/12/11 18:45
|
||||
@Author : alexanderwu
|
||||
@File : action_node.py
|
||||
|
||||
NOTE: You should use typing.List instead of list to do type annotation. Because in the markdown extraction process,
|
||||
we can use typing to extract the type of the node, but we cannot use built-in list to extract.
|
||||
"""
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional, Tuple, Type
|
||||
|
||||
from pydantic import BaseModel, create_model, root_validator, validator
|
||||
from tenacity import retry, stop_after_attempt, wait_random_exponential
|
||||
|
||||
from metagpt.llm import BaseGPTAPI
|
||||
from metagpt.logs import logger
|
||||
from metagpt.provider.postprecess.llm_output_postprecess import llm_output_postprecess
|
||||
from metagpt.utils.common import OutputParser, general_after_log
|
||||
|
||||
TAG = "CONTENT"
|
||||
|
||||
LANGUAGE_CONSTRAINT = "Language: Please use the same language as the user input."
|
||||
FORMAT_CONSTRAINT = f"Format: output wrapped inside [{TAG}][/{TAG}] like format example, nothing else."
|
||||
|
||||
|
||||
SIMPLE_TEMPLATE = """
|
||||
## context
|
||||
{context}
|
||||
|
||||
-----
|
||||
|
||||
## format example
|
||||
{example}
|
||||
|
||||
## nodes: "<node>: <type> # <instruction>"
|
||||
{instruction}
|
||||
|
||||
## constraint
|
||||
{constraint}
|
||||
|
||||
## action
|
||||
Follow instructions of nodes, generate output and make sure it follows the format example.
|
||||
"""
|
||||
|
||||
|
||||
def dict_to_markdown(d, prefix="- ", kv_sep="\n", postfix="\n"):
|
||||
markdown_str = ""
|
||||
for key, value in d.items():
|
||||
markdown_str += f"{prefix}{key}{kv_sep}{value}{postfix}"
|
||||
return markdown_str
|
||||
|
||||
|
||||
class ActionNode:
|
||||
"""ActionNode is a tree of nodes."""
|
||||
|
||||
mode: str
|
||||
|
||||
# Action Context
|
||||
context: str # all the context, including all necessary info
|
||||
llm: BaseGPTAPI # LLM with aask interface
|
||||
children: dict[str, "ActionNode"]
|
||||
|
||||
# Action Input
|
||||
key: str # Product Requirement / File list / Code
|
||||
expected_type: Type # such as str / int / float etc.
|
||||
# context: str # everything in the history.
|
||||
instruction: str # the instructions should be followed.
|
||||
example: Any # example for In Context-Learning.
|
||||
|
||||
# Action Output
|
||||
content: str
|
||||
instruct_content: BaseModel
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
key: str,
|
||||
expected_type: Type,
|
||||
instruction: str,
|
||||
example: Any,
|
||||
content: str = "",
|
||||
children: dict[str, "ActionNode"] = None,
|
||||
):
|
||||
self.key = key
|
||||
self.expected_type = expected_type
|
||||
self.instruction = instruction
|
||||
self.example = example
|
||||
self.content = content
|
||||
self.children = children if children is not None else {}
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"{self.key}, {self.expected_type}, {self.instruction}, {self.example}" f", {self.content}, {self.children}"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def add_child(self, node: "ActionNode"):
|
||||
"""增加子ActionNode"""
|
||||
self.children[node.key] = node
|
||||
|
||||
def add_children(self, nodes: List["ActionNode"]):
|
||||
"""批量增加子ActionNode"""
|
||||
for node in nodes:
|
||||
self.add_child(node)
|
||||
|
||||
@classmethod
|
||||
def from_children(cls, key, nodes: List["ActionNode"]):
|
||||
"""直接从一系列的子nodes初始化"""
|
||||
obj = cls(key, str, "", "")
|
||||
obj.add_children(nodes)
|
||||
return obj
|
||||
|
||||
def get_children_mapping(self) -> Dict[str, Tuple[Type, Any]]:
|
||||
"""获得子ActionNode的字典,以key索引"""
|
||||
return {k: (v.expected_type, ...) for k, v in self.children.items()}
|
||||
|
||||
def get_self_mapping(self) -> Dict[str, Tuple[Type, Any]]:
|
||||
"""get self key: type mapping"""
|
||||
return {self.key: (self.expected_type, ...)}
|
||||
|
||||
def get_mapping(self, mode="children") -> Dict[str, Tuple[Type, Any]]:
|
||||
"""get key: type mapping under mode"""
|
||||
if mode == "children" or (mode == "auto" and self.children):
|
||||
return self.get_children_mapping()
|
||||
return self.get_self_mapping()
|
||||
|
||||
@classmethod
|
||||
def create_model_class(cls, class_name: str, mapping: Dict[str, Tuple[Type, Any]]):
|
||||
"""基于pydantic v1的模型动态生成,用来检验结果类型正确性"""
|
||||
new_class = create_model(class_name, **mapping)
|
||||
|
||||
@validator("*", allow_reuse=True)
|
||||
def check_name(v, field):
|
||||
if field.name not in mapping.keys():
|
||||
raise ValueError(f"Unrecognized block: {field.name}")
|
||||
return v
|
||||
|
||||
@root_validator(pre=True, allow_reuse=True)
|
||||
def check_missing_fields(values):
|
||||
required_fields = set(mapping.keys())
|
||||
missing_fields = required_fields - set(values.keys())
|
||||
if missing_fields:
|
||||
raise ValueError(f"Missing fields: {missing_fields}")
|
||||
return values
|
||||
|
||||
new_class.__validator_check_name = classmethod(check_name)
|
||||
new_class.__root_validator_check_missing_fields = classmethod(check_missing_fields)
|
||||
return new_class
|
||||
|
||||
def create_children_class(self):
|
||||
"""使用object内有的字段直接生成model_class"""
|
||||
class_name = f"{self.key}_AN"
|
||||
mapping = self.get_children_mapping()
|
||||
return self.create_model_class(class_name, mapping)
|
||||
|
||||
def to_dict(self, format_func=None, mode="auto") -> Dict:
|
||||
"""将当前节点与子节点都按照node: format的格式组织成字典"""
|
||||
|
||||
# 如果没有提供格式化函数,使用默认的格式化方式
|
||||
if format_func is None:
|
||||
format_func = lambda node: f"{node.instruction}"
|
||||
|
||||
# 使用提供的格式化函数来格式化当前节点的值
|
||||
formatted_value = format_func(self)
|
||||
|
||||
# 创建当前节点的键值对
|
||||
if mode == "children" or (mode == "auto" and self.children):
|
||||
node_dict = {}
|
||||
else:
|
||||
node_dict = {self.key: formatted_value}
|
||||
|
||||
if mode == "root":
|
||||
return node_dict
|
||||
|
||||
# 遍历子节点并递归调用 to_dict 方法
|
||||
for _, child_node in self.children.items():
|
||||
node_dict.update(child_node.to_dict(format_func))
|
||||
|
||||
return node_dict
|
||||
|
||||
def compile_to(self, i: Dict, schema, kv_sep) -> str:
|
||||
if schema == "json":
|
||||
return json.dumps(i, indent=4)
|
||||
elif schema == "markdown":
|
||||
return dict_to_markdown(i, kv_sep=kv_sep)
|
||||
else:
|
||||
return str(i)
|
||||
|
||||
def tagging(self, text, schema, tag="") -> str:
|
||||
if not tag:
|
||||
return text
|
||||
if schema == "json":
|
||||
return f"[{tag}]\n" + text + f"\n[/{tag}]"
|
||||
else: # markdown
|
||||
return f"[{tag}]\n" + text + f"\n[/{tag}]"
|
||||
|
||||
def _compile_f(self, schema, mode, tag, format_func, kv_sep) -> str:
|
||||
nodes = self.to_dict(format_func=format_func, mode=mode)
|
||||
text = self.compile_to(nodes, schema, kv_sep)
|
||||
return self.tagging(text, schema, tag)
|
||||
|
||||
def compile_instruction(self, schema="markdown", mode="children", tag="") -> str:
|
||||
"""compile to raw/json/markdown template with all/root/children nodes"""
|
||||
format_func = lambda i: f"{i.expected_type} # {i.instruction}"
|
||||
return self._compile_f(schema, mode, tag, format_func, kv_sep=": ")
|
||||
|
||||
def compile_example(self, schema="json", mode="children", tag="") -> str:
|
||||
"""compile to raw/json/markdown examples with all/root/children nodes"""
|
||||
|
||||
# 这里不能使用f-string,因为转译为str后再json.dumps会额外加上引号,无法作为有效的example
|
||||
# 错误示例:"File list": "['main.py', 'const.py', 'game.py']", 注意这里值不是list,而是str
|
||||
format_func = lambda i: i.example
|
||||
return self._compile_f(schema, mode, tag, format_func, kv_sep="\n")
|
||||
|
||||
def compile(self, context, schema="json", mode="children", template=SIMPLE_TEMPLATE) -> str:
|
||||
"""
|
||||
mode: all/root/children
|
||||
mode="children": 编译所有子节点为一个统一模板,包括instruction与example
|
||||
mode="all": NotImplemented
|
||||
mode="root": NotImplemented
|
||||
"""
|
||||
|
||||
# FIXME: json instruction会带来格式问题,如:"Project name": "web_2048 # 项目名称使用下划线",
|
||||
# compile example暂时不支持markdown
|
||||
self.instruction = self.compile_instruction(schema="markdown", mode=mode)
|
||||
self.example = self.compile_example(schema=schema, tag=TAG, mode=mode)
|
||||
# nodes = ", ".join(self.to_dict(mode=mode).keys())
|
||||
constraints = [LANGUAGE_CONSTRAINT, FORMAT_CONSTRAINT]
|
||||
constraint = "\n".join(constraints)
|
||||
|
||||
prompt = template.format(
|
||||
context=context,
|
||||
example=self.example,
|
||||
instruction=self.instruction,
|
||||
constraint=constraint,
|
||||
)
|
||||
return prompt
|
||||
|
||||
@retry(
|
||||
wait=wait_random_exponential(min=1, max=20),
|
||||
stop=stop_after_attempt(6),
|
||||
after=general_after_log(logger),
|
||||
)
|
||||
async def _aask_v1(
|
||||
self,
|
||||
prompt: str,
|
||||
output_class_name: str,
|
||||
output_data_mapping: dict,
|
||||
system_msgs: Optional[list[str]] = None,
|
||||
schema="markdown", # compatible to original format
|
||||
) -> (str, BaseModel):
|
||||
"""Use ActionOutput to wrap the output of aask"""
|
||||
content = await self.llm.aask(prompt, system_msgs)
|
||||
logger.debug(f"llm raw output:\n{content}")
|
||||
output_class = self.create_model_class(output_class_name, output_data_mapping)
|
||||
|
||||
if schema == "json":
|
||||
parsed_data = llm_output_postprecess(output=content, schema=output_class.schema(), req_key=f"[/{TAG}]")
|
||||
else: # using markdown parser
|
||||
parsed_data = OutputParser.parse_data_with_mapping(content, output_data_mapping)
|
||||
|
||||
logger.debug(f"parsed_data:\n{parsed_data}")
|
||||
instruct_content = output_class(**parsed_data)
|
||||
return content, instruct_content
|
||||
|
||||
def get(self, key):
|
||||
return self.instruct_content.dict()[key]
|
||||
|
||||
def set_recursive(self, name, value):
|
||||
setattr(self, name, value)
|
||||
for _, i in self.children.items():
|
||||
i.set_recursive(name, value)
|
||||
|
||||
def set_llm(self, llm):
|
||||
self.set_recursive("llm", llm)
|
||||
|
||||
def set_context(self, context):
|
||||
self.set_recursive("context", context)
|
||||
|
||||
async def simple_fill(self, schema, mode):
|
||||
prompt = self.compile(context=self.context, schema=schema, mode=mode)
|
||||
mapping = self.get_mapping(mode)
|
||||
|
||||
class_name = f"{self.key}_AN"
|
||||
content, scontent = await self._aask_v1(prompt, class_name, mapping, schema=schema)
|
||||
self.content = content
|
||||
self.instruct_content = scontent
|
||||
return self
|
||||
|
||||
async def fill(self, context, llm, schema="json", mode="auto", strgy="simple"):
|
||||
"""Fill the node(s) with mode.
|
||||
|
||||
:param context: Everything we should know when filling node.
|
||||
:param llm: Large Language Model with pre-defined system message.
|
||||
:param schema: json/markdown, determine example and output format.
|
||||
- json: it's easy to open source LLM with json format
|
||||
- markdown: when generating code, markdown is always better
|
||||
:param mode: auto/children/root
|
||||
- auto: automated fill children's nodes and gather outputs, if no children, fill itself
|
||||
- children: fill children's nodes and gather outputs
|
||||
- root: fill root's node and gather output
|
||||
:param strgy: simple/complex
|
||||
- simple: run only once
|
||||
- complex: run each node
|
||||
:return: self
|
||||
"""
|
||||
self.set_llm(llm)
|
||||
self.set_context(context)
|
||||
|
||||
if strgy == "simple":
|
||||
return await self.simple_fill(schema, mode)
|
||||
elif strgy == "complex":
|
||||
# 这里隐式假设了拥有children
|
||||
tmp = {}
|
||||
for _, i in self.children.items():
|
||||
child = await i.simple_fill(schema, mode)
|
||||
tmp.update(child.instruct_content.dict())
|
||||
cls = self.create_children_class()
|
||||
self.instruct_content = cls(**tmp)
|
||||
return self
|
||||
|
||||
|
||||
def action_node_from_tuple_example():
|
||||
# 示例:列表中包含元组
|
||||
list_of_tuples = [("key1", str, "Instruction 1", "Example 1")]
|
||||
|
||||
# 从列表中创建 ActionNode 实例
|
||||
nodes = [ActionNode(*data) for data in list_of_tuples]
|
||||
for i in nodes:
|
||||
logger.info(i)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
action_node_from_tuple_example()
|
||||
|
|
@ -6,9 +6,7 @@
|
|||
@File : action_output
|
||||
"""
|
||||
|
||||
from typing import Dict, Type
|
||||
|
||||
from pydantic import BaseModel, create_model, root_validator, validator
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ActionOutput:
|
||||
|
|
@ -18,26 +16,3 @@ class ActionOutput:
|
|||
def __init__(self, content: str, instruct_content: BaseModel):
|
||||
self.content = content
|
||||
self.instruct_content = instruct_content
|
||||
|
||||
@classmethod
|
||||
def create_model_class(cls, class_name: str, mapping: Dict[str, Type]):
|
||||
new_class = create_model(class_name, **mapping)
|
||||
|
||||
@validator('*', allow_reuse=True)
|
||||
def check_name(v, field):
|
||||
if field.name not in mapping.keys():
|
||||
raise ValueError(f'Unrecognized block: {field.name}')
|
||||
return v
|
||||
|
||||
@root_validator(pre=True, allow_reuse=True)
|
||||
def check_missing_fields(values):
|
||||
required_fields = set(mapping.keys())
|
||||
missing_fields = required_fields - set(values.keys())
|
||||
if missing_fields:
|
||||
raise ValueError(f'Missing fields: {missing_fields}')
|
||||
return values
|
||||
|
||||
new_class.__validator_check_name = classmethod(check_name)
|
||||
new_class.__root_validator_check_missing_fields = classmethod(check_missing_fields)
|
||||
return new_class
|
||||
|
||||
|
|
@ -8,7 +8,8 @@
|
|||
from metagpt.actions import Action
|
||||
|
||||
|
||||
class BossRequirement(Action):
|
||||
"""Boss Requirement without any implementation details"""
|
||||
class UserRequirement(Action):
|
||||
"""User Requirement without any implementation details"""
|
||||
|
||||
async def run(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/5/19 12:01
|
||||
@Author : alexanderwu
|
||||
@File : analyze_dep_libs.py
|
||||
"""
|
||||
|
||||
from metagpt.actions import Action
|
||||
|
||||
PROMPT = """You are an AI developer, trying to write a program that generates code for users based on their intentions.
|
||||
|
||||
For the user's prompt:
|
||||
|
||||
---
|
||||
The API is: {prompt}
|
||||
---
|
||||
|
||||
We decide the generated files are: {filepaths_string}
|
||||
|
||||
Now that we have a file list, we need to understand the shared dependencies they have.
|
||||
Please list and briefly describe the shared contents between the files we are generating, including exported variables,
|
||||
data patterns, id names of all DOM elements that javascript functions will use, message names and function names.
|
||||
Focus only on the names of shared dependencies, do not add any other explanations.
|
||||
"""
|
||||
|
||||
|
||||
class AnalyzeDepLibs(Action):
|
||||
def __init__(self, name, context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
self.desc = "Analyze the runtime dependencies of the program based on the context"
|
||||
|
||||
async def run(self, requirement, filepaths_string):
|
||||
# prompt = f"Below is the product requirement document (PRD):\n\n{prd}\n\n{PROMPT}"
|
||||
prompt = PROMPT.format(prompt=requirement, filepaths_string=filepaths_string)
|
||||
design_filenames = await self._aask(prompt)
|
||||
return design_filenames
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/6/9 22:22
|
||||
@Author : Leo Xiao
|
||||
@File : azure_tts.py
|
||||
"""
|
||||
from azure.cognitiveservices.speech import AudioConfig, SpeechConfig, SpeechSynthesizer
|
||||
|
||||
from metagpt.actions.action import Action
|
||||
from metagpt.config import Config
|
||||
|
||||
|
||||
class AzureTTS(Action):
|
||||
def __init__(self, name, context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
self.config = Config()
|
||||
|
||||
# Parameters reference: https://learn.microsoft.com/zh-cn/azure/cognitive-services/speech-service/language-support?tabs=tts#voice-styles-and-roles
|
||||
def synthesize_speech(self, lang, voice, role, text, output_file):
|
||||
subscription_key = self.config.get('AZURE_TTS_SUBSCRIPTION_KEY')
|
||||
region = self.config.get('AZURE_TTS_REGION')
|
||||
speech_config = SpeechConfig(
|
||||
subscription=subscription_key, region=region)
|
||||
|
||||
speech_config.speech_synthesis_voice_name = voice
|
||||
audio_config = AudioConfig(filename=output_file)
|
||||
synthesizer = SpeechSynthesizer(
|
||||
speech_config=speech_config,
|
||||
audio_config=audio_config)
|
||||
|
||||
# if voice=="zh-CN-YunxiNeural":
|
||||
ssml_string = f"""
|
||||
<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='{lang}' xmlns:mstts='http://www.w3.org/2001/mstts'>
|
||||
<voice name='{voice}'>
|
||||
<mstts:express-as style='affectionate' role='{role}'>
|
||||
{text}
|
||||
</mstts:express-as>
|
||||
</voice>
|
||||
</speak>
|
||||
"""
|
||||
|
||||
synthesizer.speak_ssml_async(ssml_string).get()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
azure_tts = AzureTTS("azure_tts")
|
||||
azure_tts.synthesize_speech(
|
||||
"zh-CN",
|
||||
"zh-CN-YunxiNeural",
|
||||
"Boy",
|
||||
"Hello, I am Kaka",
|
||||
"output.wav")
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
from pathlib import Path
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
from metagpt.actions.write_code import WriteCode
|
||||
from metagpt.logs import logger
|
||||
|
|
@ -42,7 +42,7 @@ class CloneFunction(WriteCode):
|
|||
prompt = CLONE_PROMPT.format(source_code=source_code, template_func=template_func)
|
||||
logger.info(f"query for CloneFunction: \n {prompt}")
|
||||
code = await self.write_code(prompt)
|
||||
logger.info(f'CloneFunction code is \n {highlight(code)}')
|
||||
logger.info(f"CloneFunction code is \n {highlight(code)}")
|
||||
return code
|
||||
|
||||
|
||||
|
|
@ -61,5 +61,5 @@ def run_function_script(code_script_path: str, func_name: str, *args, **kwargs):
|
|||
"""Run function code from script."""
|
||||
if isinstance(code_script_path, str):
|
||||
code_path = Path(code_script_path)
|
||||
code = code_path.read_text(encoding='utf-8')
|
||||
code = code_path.read_text(encoding="utf-8")
|
||||
return run_function_code(code, func_name, *args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -4,12 +4,22 @@
|
|||
@Time : 2023/5/11 17:46
|
||||
@Author : alexanderwu
|
||||
@File : debug_error.py
|
||||
@Modified By: mashenquan, 2023/11/27.
|
||||
1. Divide the context into three components: legacy code, unit test code, and console log.
|
||||
2. According to Section 2.2.3.1 of RFC 135, replace file data in the message with the file name.
|
||||
"""
|
||||
import re
|
||||
|
||||
from metagpt.logs import logger
|
||||
from pydantic import Field
|
||||
|
||||
from metagpt.actions.action import Action
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.const import TEST_CODES_FILE_REPO, TEST_OUTPUTS_FILE_REPO
|
||||
from metagpt.llm import LLM, BaseGPTAPI
|
||||
from metagpt.logs import logger
|
||||
from metagpt.schema import RunCodeContext, RunCodeResult
|
||||
from metagpt.utils.common import CodeParser
|
||||
from metagpt.utils.file_repository import FileRepository
|
||||
|
||||
PROMPT_TEMPLATE = """
|
||||
NOTICE
|
||||
|
|
@ -19,33 +29,57 @@ Based on the message, first, figure out your own role, i.e. Engineer or QaEngine
|
|||
then rewrite the development code or the test code based on your role, the error, and the summary, such that all bugs are fixed and the code performs well.
|
||||
Attention: Use '##' to split sections, not '#', and '## <SECTION_NAME>' SHOULD WRITE BEFORE the test case or script and triple quotes.
|
||||
The message is as follows:
|
||||
{context}
|
||||
# Legacy Code
|
||||
```python
|
||||
{code}
|
||||
```
|
||||
---
|
||||
# Unit Test Code
|
||||
```python
|
||||
{test_code}
|
||||
```
|
||||
---
|
||||
# Console logs
|
||||
```text
|
||||
{logs}
|
||||
```
|
||||
---
|
||||
Now you should start rewriting the code:
|
||||
## file name of the code to rewrite: Write code with triple quoto. Do your best to implement THIS IN ONLY ONE FILE.
|
||||
## file name of the code to rewrite: Write code with triple quote. Do your best to implement THIS IN ONLY ONE FILE.
|
||||
"""
|
||||
|
||||
|
||||
class DebugError(Action):
|
||||
def __init__(self, name="DebugError", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
name: str = "DebugError"
|
||||
context: RunCodeContext = Field(default_factory=RunCodeContext)
|
||||
llm: BaseGPTAPI = Field(default_factory=LLM)
|
||||
|
||||
# async def run(self, code, error):
|
||||
# prompt = f"Here is a piece of Python code:\n\n{code}\n\nThe following error occurred during execution:" \
|
||||
# f"\n\n{error}\n\nPlease try to fix the error in this code."
|
||||
# fixed_code = await self._aask(prompt)
|
||||
# return fixed_code
|
||||
|
||||
async def run(self, context):
|
||||
if "PASS" in context:
|
||||
return "", "the original code works fine, no need to debug"
|
||||
|
||||
file_name = re.search("## File To Rewrite:\s*(.+\\.py)", context).group(1)
|
||||
async def run(self, *args, **kwargs) -> str:
|
||||
output_doc = await FileRepository.get_file(
|
||||
filename=self.context.output_filename, relative_path=TEST_OUTPUTS_FILE_REPO
|
||||
)
|
||||
if not output_doc:
|
||||
return ""
|
||||
output_detail = RunCodeResult.loads(output_doc.content)
|
||||
pattern = r"Ran (\d+) tests in ([\d.]+)s\n\nOK"
|
||||
matches = re.search(pattern, output_detail.stderr)
|
||||
if matches:
|
||||
return ""
|
||||
|
||||
logger.info(f"Debug and rewrite {file_name}")
|
||||
logger.info(f"Debug and rewrite {self.context.test_filename}")
|
||||
code_doc = await FileRepository.get_file(
|
||||
filename=self.context.code_filename, relative_path=CONFIG.src_workspace
|
||||
)
|
||||
if not code_doc:
|
||||
return ""
|
||||
test_doc = await FileRepository.get_file(
|
||||
filename=self.context.test_filename, relative_path=TEST_CODES_FILE_REPO
|
||||
)
|
||||
if not test_doc:
|
||||
return ""
|
||||
prompt = PROMPT_TEMPLATE.format(code=code_doc.content, test_code=test_doc.content, logs=output_detail.stderr)
|
||||
|
||||
prompt = PROMPT_TEMPLATE.format(context=context)
|
||||
|
||||
rsp = await self._aask(prompt)
|
||||
|
||||
code = CodeParser.parse_code(block="", text=rsp)
|
||||
|
||||
return file_name, code
|
||||
return code
|
||||
|
|
|
|||
|
|
@ -4,214 +4,138 @@
|
|||
@Time : 2023/5/11 19:26
|
||||
@Author : alexanderwu
|
||||
@File : design_api.py
|
||||
@Modified By: mashenquan, 2023/11/27.
|
||||
1. According to Section 2.2.3.1 of RFC 135, replace file data in the message with the file name.
|
||||
2. According to the design in Section 2.2.3.5.3 of RFC 135, add incremental iteration functionality.
|
||||
@Modified By: mashenquan, 2023/12/5. Move the generation logic of the project name to WritePRD.
|
||||
"""
|
||||
import shutil
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from metagpt.actions import Action, ActionOutput
|
||||
from metagpt.actions.design_api_an import DESIGN_API_NODE
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.const import WORKSPACE_ROOT
|
||||
from metagpt.const import (
|
||||
DATA_API_DESIGN_FILE_REPO,
|
||||
PRDS_FILE_REPO,
|
||||
SEQ_FLOW_FILE_REPO,
|
||||
SYSTEM_DESIGN_FILE_REPO,
|
||||
SYSTEM_DESIGN_PDF_FILE_REPO,
|
||||
)
|
||||
from metagpt.llm import LLM
|
||||
from metagpt.logs import logger
|
||||
from metagpt.utils.common import CodeParser
|
||||
from metagpt.utils.get_template import get_template
|
||||
from metagpt.utils.json_to_markdown import json_to_markdown
|
||||
from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
from metagpt.schema import Document, Documents, Message
|
||||
from metagpt.utils.file_repository import FileRepository
|
||||
from metagpt.utils.mermaid import mermaid_to_file
|
||||
|
||||
templates = {
|
||||
"json": {
|
||||
"PROMPT_TEMPLATE": """
|
||||
# Context
|
||||
NEW_REQ_TEMPLATE = """
|
||||
### Legacy Content
|
||||
{old_design}
|
||||
|
||||
### New Requirements
|
||||
{context}
|
||||
|
||||
## Format example
|
||||
{format_example}
|
||||
-----
|
||||
Role: You are an architect; the goal is to design a SOTA PEP8-compliant python system; make the best use of good open source tools
|
||||
Requirement: Fill in the following missing information based on the context, each section name is a key in json
|
||||
Max Output: 8192 chars or 2048 tokens. Try to use them up.
|
||||
|
||||
## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework.
|
||||
|
||||
## Python package name: Provide as Python str with python triple quoto, concise and clear, characters only use a combination of all lowercase and underscores
|
||||
|
||||
## File list: Provided as Python list[str], the list of ONLY REQUIRED files needed to write the program(LESS IS MORE!). Only need relative paths, comply with PEP8 standards. ALWAYS write a main.py or app.py here
|
||||
|
||||
## Data structures and interface definitions: Use mermaid classDiagram code syntax, including classes (INCLUDING __init__ method) and functions (with type annotations), CLEARLY MARK the RELATIONSHIPS between classes, and comply with PEP8 standards. The data structures SHOULD BE VERY DETAILED and the API should be comprehensive with a complete design.
|
||||
|
||||
## Program call flow: Use sequenceDiagram code syntax, COMPLETE and VERY DETAILED, using CLASSES AND API DEFINED ABOVE accurately, covering the CRUD AND INIT of each object, SYNTAX MUST BE CORRECT.
|
||||
|
||||
## Anything UNCLEAR: Provide as Plain text. Make clear here.
|
||||
|
||||
output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example,
|
||||
and only output the json inside this tag, nothing else
|
||||
""",
|
||||
"FORMAT_EXAMPLE": """
|
||||
[CONTENT]
|
||||
{
|
||||
"Implementation approach": "We will ...",
|
||||
"Python package name": "snake_game",
|
||||
"File list": ["main.py"],
|
||||
"Data structures and interface definitions": '
|
||||
classDiagram
|
||||
class Game{
|
||||
+int score
|
||||
}
|
||||
...
|
||||
Game "1" -- "1" Food: has
|
||||
',
|
||||
"Program call flow": '
|
||||
sequenceDiagram
|
||||
participant M as Main
|
||||
...
|
||||
G->>M: end game
|
||||
',
|
||||
"Anything UNCLEAR": "The requirement is clear to me."
|
||||
}
|
||||
[/CONTENT]
|
||||
""",
|
||||
},
|
||||
"markdown": {
|
||||
"PROMPT_TEMPLATE": """
|
||||
# Context
|
||||
{context}
|
||||
|
||||
## Format example
|
||||
{format_example}
|
||||
-----
|
||||
Role: You are an architect; the goal is to design a SOTA PEP8-compliant python system; make the best use of good open source tools
|
||||
Requirement: Fill in the following missing information based on the context, note that all sections are response with code form separately
|
||||
Max Output: 8192 chars or 2048 tokens. Try to use them up.
|
||||
Attention: Use '##' to split sections, not '#', and '## <SECTION_NAME>' SHOULD WRITE BEFORE the code and triple quote.
|
||||
|
||||
## Implementation approach: Provide as Plain text. Analyze the difficult points of the requirements, select the appropriate open-source framework.
|
||||
|
||||
## Python package name: Provide as Python str with python triple quoto, concise and clear, characters only use a combination of all lowercase and underscores
|
||||
|
||||
## File list: Provided as Python list[str], the list of ONLY REQUIRED files needed to write the program(LESS IS MORE!). Only need relative paths, comply with PEP8 standards. ALWAYS write a main.py or app.py here
|
||||
|
||||
## Data structures and interface definitions: Use mermaid classDiagram code syntax, including classes (INCLUDING __init__ method) and functions (with type annotations), CLEARLY MARK the RELATIONSHIPS between classes, and comply with PEP8 standards. The data structures SHOULD BE VERY DETAILED and the API should be comprehensive with a complete design.
|
||||
|
||||
## Program call flow: Use sequenceDiagram code syntax, COMPLETE and VERY DETAILED, using CLASSES AND API DEFINED ABOVE accurately, covering the CRUD AND INIT of each object, SYNTAX MUST BE CORRECT.
|
||||
|
||||
## Anything UNCLEAR: Provide as Plain text. Make clear here.
|
||||
|
||||
""",
|
||||
"FORMAT_EXAMPLE": """
|
||||
---
|
||||
## Implementation approach
|
||||
We will ...
|
||||
|
||||
## Python package name
|
||||
```python
|
||||
"snake_game"
|
||||
```
|
||||
|
||||
## File list
|
||||
```python
|
||||
[
|
||||
"main.py",
|
||||
]
|
||||
```
|
||||
|
||||
## Data structures and interface definitions
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Game{
|
||||
+int score
|
||||
}
|
||||
...
|
||||
Game "1" -- "1" Food: has
|
||||
```
|
||||
|
||||
## Program call flow
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant M as Main
|
||||
...
|
||||
G->>M: end game
|
||||
```
|
||||
|
||||
## Anything UNCLEAR
|
||||
The requirement is clear to me.
|
||||
---
|
||||
""",
|
||||
},
|
||||
}
|
||||
|
||||
OUTPUT_MAPPING = {
|
||||
"Implementation approach": (str, ...),
|
||||
"Python package name": (str, ...),
|
||||
"File list": (List[str], ...),
|
||||
"Data structures and interface definitions": (str, ...),
|
||||
"Program call flow": (str, ...),
|
||||
"Anything UNCLEAR": (str, ...),
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class WriteDesign(Action):
|
||||
def __init__(self, name, context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
self.desc = (
|
||||
"Based on the PRD, think about the system design, and design the corresponding APIs, "
|
||||
"data structures, library tables, processes, and paths. Please provide your design, feedback "
|
||||
"clearly and in detail."
|
||||
)
|
||||
name: str = ""
|
||||
context: Optional[str] = None
|
||||
llm: BaseGPTAPI = Field(default_factory=LLM)
|
||||
desc: str = (
|
||||
"Based on the PRD, think about the system design, and design the corresponding APIs, "
|
||||
"data structures, library tables, processes, and paths. Please provide your design, feedback "
|
||||
"clearly and in detail."
|
||||
)
|
||||
|
||||
def recreate_workspace(self, workspace: Path):
|
||||
try:
|
||||
shutil.rmtree(workspace)
|
||||
except FileNotFoundError:
|
||||
pass # Folder does not exist, but we don't care
|
||||
workspace.mkdir(parents=True, exist_ok=True)
|
||||
async def run(self, with_messages: Message, schema: str = CONFIG.prompt_schema):
|
||||
# Use `git diff` to identify which PRD documents have been modified in the `docs/prds` directory.
|
||||
prds_file_repo = CONFIG.git_repo.new_file_repository(PRDS_FILE_REPO)
|
||||
changed_prds = prds_file_repo.changed_files
|
||||
# Use `git diff` to identify which design documents in the `docs/system_designs` directory have undergone
|
||||
# changes.
|
||||
system_design_file_repo = CONFIG.git_repo.new_file_repository(SYSTEM_DESIGN_FILE_REPO)
|
||||
changed_system_designs = system_design_file_repo.changed_files
|
||||
|
||||
async def _save_prd(self, docs_path, resources_path, context):
|
||||
prd_file = docs_path / "prd.md"
|
||||
if context[-1].instruct_content and context[-1].instruct_content.dict()["Competitive Quadrant Chart"]:
|
||||
quadrant_chart = context[-1].instruct_content.dict()["Competitive Quadrant Chart"]
|
||||
await mermaid_to_file(quadrant_chart, resources_path / "competitive_analysis")
|
||||
# For those PRDs and design documents that have undergone changes, regenerate the design content.
|
||||
changed_files = Documents()
|
||||
for filename in changed_prds.keys():
|
||||
doc = await self._update_system_design(
|
||||
filename=filename, prds_file_repo=prds_file_repo, system_design_file_repo=system_design_file_repo
|
||||
)
|
||||
changed_files.docs[filename] = doc
|
||||
|
||||
if context[-1].instruct_content:
|
||||
logger.info(f"Saving PRD to {prd_file}")
|
||||
prd_file.write_text(json_to_markdown(context[-1].instruct_content.dict()))
|
||||
for filename in changed_system_designs.keys():
|
||||
if filename in changed_files.docs:
|
||||
continue
|
||||
doc = await self._update_system_design(
|
||||
filename=filename, prds_file_repo=prds_file_repo, system_design_file_repo=system_design_file_repo
|
||||
)
|
||||
changed_files.docs[filename] = doc
|
||||
if not changed_files.docs:
|
||||
logger.info("Nothing has changed.")
|
||||
# Wait until all files under `docs/system_designs/` are processed before sending the publish message,
|
||||
# leaving room for global optimization in subsequent steps.
|
||||
return ActionOutput(content=changed_files.json(), instruct_content=changed_files)
|
||||
|
||||
async def _save_system_design(self, docs_path, resources_path, system_design):
|
||||
data_api_design = system_design.instruct_content.dict()[
|
||||
"Data structures and interface definitions"
|
||||
] # CodeParser.parse_code(block="Data structures and interface definitions", text=content)
|
||||
seq_flow = system_design.instruct_content.dict()[
|
||||
"Program call flow"
|
||||
] # CodeParser.parse_code(block="Program call flow", text=content)
|
||||
await mermaid_to_file(data_api_design, resources_path / "data_api_design")
|
||||
await mermaid_to_file(seq_flow, resources_path / "seq_flow")
|
||||
system_design_file = docs_path / "system_design.md"
|
||||
logger.info(f"Saving System Designs to {system_design_file}")
|
||||
system_design_file.write_text((json_to_markdown(system_design.instruct_content.dict())))
|
||||
async def _new_system_design(self, context, schema=CONFIG.prompt_schema):
|
||||
node = await DESIGN_API_NODE.fill(context=context, llm=self.llm, schema=schema)
|
||||
return node
|
||||
|
||||
async def _save(self, context, system_design):
|
||||
if isinstance(system_design, ActionOutput):
|
||||
ws_name = system_design.instruct_content.dict()["Python package name"]
|
||||
async def _merge(self, prd_doc, system_design_doc, schema=CONFIG.prompt_schema):
|
||||
context = NEW_REQ_TEMPLATE.format(old_design=system_design_doc.content, context=prd_doc.content)
|
||||
node = await DESIGN_API_NODE.fill(context=context, llm=self.llm, schema=schema)
|
||||
system_design_doc.content = node.instruct_content.json(ensure_ascii=False)
|
||||
return system_design_doc
|
||||
|
||||
async def _update_system_design(self, filename, prds_file_repo, system_design_file_repo) -> Document:
|
||||
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._new_system_design(context=prd.content)
|
||||
doc = Document(
|
||||
root_path=SYSTEM_DESIGN_FILE_REPO,
|
||||
filename=filename,
|
||||
content=system_design.instruct_content.json(ensure_ascii=False),
|
||||
)
|
||||
else:
|
||||
ws_name = CodeParser.parse_str(block="Python package name", text=system_design)
|
||||
workspace = WORKSPACE_ROOT / ws_name
|
||||
self.recreate_workspace(workspace)
|
||||
docs_path = workspace / "docs"
|
||||
resources_path = workspace / "resources"
|
||||
docs_path.mkdir(parents=True, exist_ok=True)
|
||||
resources_path.mkdir(parents=True, exist_ok=True)
|
||||
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):
|
||||
prompt_template, format_example = get_template(templates, format)
|
||||
prompt = prompt_template.format(context=context, format_example=format_example)
|
||||
# system_design = await self._aask(prompt)
|
||||
system_design = await self._aask_v1(prompt, "system_design", OUTPUT_MAPPING, format=format)
|
||||
# fix Python package name, we can't system_design.instruct_content.python_package_name = "xxx" since "Python package name" contain space, have to use setattr
|
||||
setattr(
|
||||
system_design.instruct_content,
|
||||
"Python package name",
|
||||
system_design.instruct_content.dict()["Python package name"].strip().strip("'").strip('"'),
|
||||
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}
|
||||
)
|
||||
await self._save(context, system_design)
|
||||
return system_design
|
||||
await self._save_data_api_design(doc)
|
||||
await self._save_seq_flow(doc)
|
||||
await self._save_pdf(doc)
|
||||
return doc
|
||||
|
||||
@staticmethod
|
||||
async def _save_data_api_design(design_doc):
|
||||
m = json.loads(design_doc.content)
|
||||
data_api_design = m.get("Data structures and interfaces")
|
||||
if not data_api_design:
|
||||
return
|
||||
pathname = CONFIG.git_repo.workdir / DATA_API_DESIGN_FILE_REPO / Path(design_doc.filename).with_suffix("")
|
||||
await WriteDesign._save_mermaid_file(data_api_design, pathname)
|
||||
logger.info(f"Save class view to {str(pathname)}")
|
||||
|
||||
@staticmethod
|
||||
async def _save_seq_flow(design_doc):
|
||||
m = json.loads(design_doc.content)
|
||||
seq_flow = m.get("Program call flow")
|
||||
if not seq_flow:
|
||||
return
|
||||
pathname = CONFIG.git_repo.workdir / Path(SEQ_FLOW_FILE_REPO) / Path(design_doc.filename).with_suffix("")
|
||||
await WriteDesign._save_mermaid_file(seq_flow, pathname)
|
||||
logger.info(f"Saving sequence flow to {str(pathname)}")
|
||||
|
||||
@staticmethod
|
||||
async def _save_pdf(design_doc):
|
||||
await FileRepository.save_as(doc=design_doc, with_suffix=".md", relative_path=SYSTEM_DESIGN_PDF_FILE_REPO)
|
||||
|
||||
@staticmethod
|
||||
async def _save_mermaid_file(data: str, pathname: Path):
|
||||
pathname.parent.mkdir(parents=True, exist_ok=True)
|
||||
await mermaid_to_file(data, pathname)
|
||||
|
|
|
|||
74
metagpt/actions/design_api_an.py
Normal file
74
metagpt/actions/design_api_an.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/12/12 22:24
|
||||
@Author : alexanderwu
|
||||
@File : design_api_an.py
|
||||
"""
|
||||
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(
|
||||
key="Implementation approach",
|
||||
expected_type=str,
|
||||
instruction="Analyze the difficult points of the requirements, select the appropriate open-source framework",
|
||||
example="We will ...",
|
||||
)
|
||||
|
||||
PROJECT_NAME = ActionNode(
|
||||
key="Project name", expected_type=str, instruction="The project name with underline", example="game_2048"
|
||||
)
|
||||
|
||||
FILE_LIST = ActionNode(
|
||||
key="File list",
|
||||
expected_type=List[str],
|
||||
instruction="Only need relative paths. ALWAYS write a main.py or app.py here",
|
||||
example=["main.py", "game.py"],
|
||||
)
|
||||
|
||||
DATA_STRUCTURES_AND_INTERFACES = ActionNode(
|
||||
key="Data structures and interfaces",
|
||||
expected_type=str,
|
||||
instruction="Use mermaid classDiagram code syntax, including classes, method(__init__ etc.) and functions with type"
|
||||
" annotations, CLEARLY MARK the RELATIONSHIPS between classes, and comply with PEP8 standards. "
|
||||
"The data structures SHOULD BE VERY DETAILED and the API should be comprehensive with a complete design.",
|
||||
example=MMC1,
|
||||
)
|
||||
|
||||
PROGRAM_CALL_FLOW = ActionNode(
|
||||
key="Program call flow",
|
||||
expected_type=str,
|
||||
instruction="Use sequenceDiagram code syntax, COMPLETE and VERY DETAILED, using CLASSES AND API DEFINED ABOVE "
|
||||
"accurately, covering the CRUD AND INIT of each object, SYNTAX MUST BE CORRECT.",
|
||||
example=MMC2,
|
||||
)
|
||||
|
||||
ANYTHING_UNCLEAR = ActionNode(
|
||||
key="Anything UNCLEAR",
|
||||
expected_type=str,
|
||||
instruction="Mention unclear project aspects, then try to clarify it.",
|
||||
example="Clarification needed on third-party API integration, ...",
|
||||
)
|
||||
|
||||
NODES = [
|
||||
IMPLEMENTATION_APPROACH,
|
||||
# PROJECT_NAME,
|
||||
FILE_LIST,
|
||||
DATA_STRUCTURES_AND_INTERFACES,
|
||||
PROGRAM_CALL_FLOW,
|
||||
ANYTHING_UNCLEAR,
|
||||
]
|
||||
|
||||
DESIGN_API_NODE = ActionNode.from_children("DesignAPI", NODES)
|
||||
|
||||
|
||||
def main():
|
||||
prompt = DESIGN_API_NODE.compile(context="")
|
||||
logger.info(prompt)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -13,10 +13,11 @@ class DesignReview(Action):
|
|||
super().__init__(name, context, llm)
|
||||
|
||||
async def run(self, prd, api_design):
|
||||
prompt = f"Here is the Product Requirement Document (PRD):\n\n{prd}\n\nHere is the list of APIs designed " \
|
||||
f"based on this PRD:\n\n{api_design}\n\nPlease review whether this API design meets the requirements" \
|
||||
f" of the PRD, and whether it complies with good design practices."
|
||||
prompt = (
|
||||
f"Here is the Product Requirement Document (PRD):\n\n{prd}\n\nHere is the list of APIs designed "
|
||||
f"based on this PRD:\n\n{api_design}\n\nPlease review whether this API design meets the requirements"
|
||||
f" of the PRD, and whether it complies with good design practices."
|
||||
)
|
||||
|
||||
api_review = await self._aask(prompt)
|
||||
return api_review
|
||||
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/5/19 11:50
|
||||
@Author : alexanderwu
|
||||
@File : design_filenames.py
|
||||
"""
|
||||
from metagpt.actions import Action
|
||||
from metagpt.logs import logger
|
||||
|
||||
PROMPT = """You are an AI developer, trying to write a program that generates code for users based on their intentions.
|
||||
When given their intentions, provide a complete and exhaustive list of file paths needed to write the program for the user.
|
||||
Only list the file paths you will write and return them as a Python string list.
|
||||
Do not add any other explanations, just return a Python string list."""
|
||||
|
||||
|
||||
class DesignFilenames(Action):
|
||||
def __init__(self, name, context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
self.desc = "Based on the PRD, consider system design, and carry out the basic design of the corresponding " \
|
||||
"APIs, data structures, and database tables. Please give your design, feedback clearly and in detail."
|
||||
|
||||
async def run(self, prd):
|
||||
prompt = f"The following is the Product Requirement Document (PRD):\n\n{prd}\n\n{PROMPT}"
|
||||
design_filenames = await self._aask(prompt)
|
||||
logger.debug(prompt)
|
||||
logger.debug(design_filenames)
|
||||
return design_filenames
|
||||
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/9/12 17:45
|
||||
@Author : fisherdeng
|
||||
@File : detail_mining.py
|
||||
"""
|
||||
from metagpt.actions import Action, ActionOutput
|
||||
from metagpt.logs import logger
|
||||
|
||||
PROMPT_TEMPLATE = """
|
||||
##TOPIC
|
||||
{topic}
|
||||
|
||||
##RECORD
|
||||
{record}
|
||||
|
||||
##Format example
|
||||
{format_example}
|
||||
-----
|
||||
|
||||
Task: Refer to the "##TOPIC" (discussion objectives) and "##RECORD" (discussion records) to further inquire about the details that interest you, within a word limit of 150 words.
|
||||
Special Note 1: Your intention is solely to ask questions without endorsing or negating any individual's viewpoints.
|
||||
Special Note 2: This output should only include the topic "##OUTPUT". Do not add, remove, or modify the topic. Begin the output with '##OUTPUT', followed by an immediate line break, and then proceed to provide the content in the specified format as outlined in the "##Format example" section.
|
||||
Special Note 3: The output should be in the same language as the input.
|
||||
"""
|
||||
FORMAT_EXAMPLE = """
|
||||
|
||||
##
|
||||
|
||||
##OUTPUT
|
||||
...(Please provide the specific details you would like to inquire about here.)
|
||||
|
||||
##
|
||||
|
||||
##
|
||||
"""
|
||||
OUTPUT_MAPPING = {
|
||||
"OUTPUT": (str, ...),
|
||||
}
|
||||
|
||||
|
||||
class DetailMining(Action):
|
||||
"""This class allows LLM to further mine noteworthy details based on specific "##TOPIC"(discussion topic) and "##RECORD" (discussion records), thereby deepening the discussion.
|
||||
"""
|
||||
def __init__(self, name="", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
|
||||
async def run(self, topic, record) -> ActionOutput:
|
||||
prompt = PROMPT_TEMPLATE.format(topic=topic, record=record, format_example=FORMAT_EXAMPLE)
|
||||
rsp = await self._aask_v1(prompt, "detail_mining", OUTPUT_MAPPING)
|
||||
return rsp
|
||||
16
metagpt/actions/fix_bug.py
Normal file
16
metagpt/actions/fix_bug.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023-12-12
|
||||
@Author : mashenquan
|
||||
@File : fix_bug.py
|
||||
"""
|
||||
from metagpt.actions import Action
|
||||
|
||||
|
||||
class FixBug(Action):
|
||||
"""Fix bug action without any implementation details"""
|
||||
|
||||
name: str = "FixBug"
|
||||
|
||||
async def run(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
25
metagpt/actions/generate_questions.py
Normal file
25
metagpt/actions/generate_questions.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/9/12 17:45
|
||||
@Author : fisherdeng
|
||||
@File : generate_questions.py
|
||||
"""
|
||||
from metagpt.actions import Action
|
||||
from metagpt.actions.action_node import ActionNode
|
||||
|
||||
QUESTIONS = ActionNode(
|
||||
key="Questions",
|
||||
expected_type=list[str],
|
||||
instruction="Task: Refer to the context to further inquire about the details that interest you, within a word limit"
|
||||
" of 150 words. Please provide the specific details you would like to inquire about here",
|
||||
example=["1. What ...", "2. How ...", "3. ..."],
|
||||
)
|
||||
|
||||
|
||||
class GenerateQuestions(Action):
|
||||
"""This class allows LLM to further mine noteworthy details based on specific "##TOPIC"(discussion topic) and
|
||||
"##RECORD" (discussion records), thereby deepening the discussion."""
|
||||
|
||||
async def run(self, context):
|
||||
return await QUESTIONS.fill(context=context, llm=self.llm)
|
||||
|
|
@ -10,8 +10,8 @@
|
|||
|
||||
import os
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
from paddleocr import PaddleOCR
|
||||
|
|
@ -19,7 +19,10 @@ from paddleocr import PaddleOCR
|
|||
from metagpt.actions import Action
|
||||
from metagpt.const import INVOICE_OCR_TABLE_PATH
|
||||
from metagpt.logs import logger
|
||||
from metagpt.prompts.invoice_ocr import EXTRACT_OCR_MAIN_INFO_PROMPT, REPLY_OCR_QUESTION_PROMPT
|
||||
from metagpt.prompts.invoice_ocr import (
|
||||
EXTRACT_OCR_MAIN_INFO_PROMPT,
|
||||
REPLY_OCR_QUESTION_PROMPT,
|
||||
)
|
||||
from metagpt.utils.common import OutputParser
|
||||
from metagpt.utils.file import File
|
||||
|
||||
|
|
@ -183,4 +186,3 @@ class ReplyQuestion(Action):
|
|||
prompt = REPLY_OCR_QUESTION_PROMPT.format(query=query, ocr_result=ocr_result, language=self.language)
|
||||
resp = await self._aask(prompt=prompt)
|
||||
return resp
|
||||
|
||||
|
|
|
|||
54
metagpt/actions/prepare_documents.py
Normal file
54
metagpt/actions/prepare_documents.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/11/20
|
||||
@Author : mashenquan
|
||||
@File : prepare_documents.py
|
||||
@Desc: PrepareDocuments Action: initialize project folder and add new requirements to docs/requirements.txt.
|
||||
RFC 135 2.2.3.5.1.
|
||||
"""
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from metagpt.actions import Action, ActionOutput
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.const import DOCS_FILE_REPO, REQUIREMENT_FILENAME
|
||||
from metagpt.llm import LLM
|
||||
from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
from metagpt.schema import Document
|
||||
from metagpt.utils.file_repository import FileRepository
|
||||
from metagpt.utils.git_repository import GitRepository
|
||||
|
||||
|
||||
class PrepareDocuments(Action):
|
||||
"""PrepareDocuments Action: initialize project folder and add new requirements to docs/requirements.txt."""
|
||||
|
||||
name: str = "PrepareDocuments"
|
||||
context: Optional[str] = None
|
||||
llm: BaseGPTAPI = Field(default_factory=LLM)
|
||||
|
||||
def _init_repo(self):
|
||||
"""Initialize the Git environment."""
|
||||
path = CONFIG.project_path
|
||||
if not path:
|
||||
name = CONFIG.project_name or FileRepository.new_filename()
|
||||
path = Path(CONFIG.workspace_path) / name
|
||||
|
||||
if path.exists() and not CONFIG.inc:
|
||||
shutil.rmtree(path)
|
||||
CONFIG.git_repo = GitRepository(local_path=path, auto_init=True)
|
||||
|
||||
async def run(self, with_messages, **kwargs):
|
||||
"""Create and initialize the workspace folder, initialize the Git environment."""
|
||||
self._init_repo()
|
||||
|
||||
# Write the newly added requirements from the main parameter idea to `docs/requirement.txt`.
|
||||
doc = Document(root_path=DOCS_FILE_REPO, filename=REQUIREMENT_FILENAME, content=with_messages[0].content)
|
||||
await FileRepository.save_file(filename=REQUIREMENT_FILENAME, content=doc.content, relative_path=DOCS_FILE_REPO)
|
||||
|
||||
# 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)
|
||||
|
|
@ -6,36 +6,18 @@
|
|||
@File : prepare_interview.py
|
||||
"""
|
||||
from metagpt.actions import Action
|
||||
from metagpt.actions.action_node import ActionNode
|
||||
|
||||
PROMPT_TEMPLATE = """
|
||||
# Context
|
||||
{context}
|
||||
|
||||
## Format example
|
||||
---
|
||||
Q1: question 1 here
|
||||
References:
|
||||
- point 1
|
||||
- point 2
|
||||
|
||||
Q2: question 2 here...
|
||||
---
|
||||
|
||||
-----
|
||||
Role: You are an interviewer of our company who is well-knonwn in frontend or backend develop;
|
||||
QUESTIONS = ActionNode(
|
||||
key="Questions",
|
||||
expected_type=list[str],
|
||||
instruction="""Role: You are an interviewer of our company who is well-knonwn in frontend or backend develop;
|
||||
Requirement: Provide a list of questions for the interviewer to ask the interviewee, by reading the resume of the interviewee in the context.
|
||||
Attention: Provide as markdown block as the format above, at least 10 questions.
|
||||
"""
|
||||
|
||||
# prepare for a interview
|
||||
Attention: Provide as markdown block as the format above, at least 10 questions.""",
|
||||
example=["1. What ...", "2. How ..."],
|
||||
)
|
||||
|
||||
|
||||
class PrepareInterview(Action):
|
||||
def __init__(self, name, context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
|
||||
async def run(self, context):
|
||||
prompt = PROMPT_TEMPLATE.format(context=context)
|
||||
question_list = await self._aask_v1(prompt)
|
||||
return question_list
|
||||
|
||||
return await QUESTIONS.fill(context=context, llm=self.llm)
|
||||
|
|
|
|||
|
|
@ -4,186 +4,125 @@
|
|||
@Time : 2023/5/11 19:12
|
||||
@Author : alexanderwu
|
||||
@File : project_management.py
|
||||
@Modified By: mashenquan, 2023/11/27.
|
||||
1. Divide the context into three components: legacy code, unit test code, and console log.
|
||||
2. Move the document storage operations related to WritePRD from the save operation of WriteDesign.
|
||||
3. According to the design in Section 2.2.3.5.4 of RFC 135, add incremental iteration functionality.
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from metagpt.actions import ActionOutput
|
||||
from metagpt.actions.action import Action
|
||||
from metagpt.actions.project_management_an import PM_NODE
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.const import WORKSPACE_ROOT
|
||||
from metagpt.utils.common import CodeParser
|
||||
from metagpt.utils.get_template import get_template
|
||||
from metagpt.utils.json_to_markdown import json_to_markdown
|
||||
from metagpt.const import (
|
||||
PACKAGE_REQUIREMENTS_FILENAME,
|
||||
SYSTEM_DESIGN_FILE_REPO,
|
||||
TASK_FILE_REPO,
|
||||
TASK_PDF_FILE_REPO,
|
||||
)
|
||||
from metagpt.llm import LLM
|
||||
from metagpt.logs import logger
|
||||
from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
from metagpt.schema import Document, Documents
|
||||
from metagpt.utils.file_repository import FileRepository
|
||||
|
||||
templates = {
|
||||
"json": {
|
||||
"PROMPT_TEMPLATE": """
|
||||
# Context
|
||||
NEW_REQ_TEMPLATE = """
|
||||
### Legacy Content
|
||||
{old_tasks}
|
||||
|
||||
### New Requirements
|
||||
{context}
|
||||
|
||||
## Format example
|
||||
{format_example}
|
||||
-----
|
||||
Role: You are a project manager; the goal is to break down tasks according to PRD/technical design, give a task list, and analyze task dependencies to start with the prerequisite modules
|
||||
Requirements: Based on the context, fill in the following missing information, each section name is a key in json. Here the granularity of the task is a file, if there are any missing files, you can supplement them
|
||||
Attention: Use '##' to split sections, not '#', and '## <SECTION_NAME>' SHOULD WRITE BEFORE the code and triple quote.
|
||||
|
||||
## Required Python third-party packages: Provided in requirements.txt format
|
||||
|
||||
## Required Other language third-party packages: Provided in requirements.txt format
|
||||
|
||||
## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend.
|
||||
|
||||
## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first
|
||||
|
||||
## Task list: Provided as Python list[str]. Each str is a filename, the more at the beginning, the more it is a prerequisite dependency, should be done first
|
||||
|
||||
## Shared Knowledge: Anything that should be public like utils' functions, config's variables details that should make clear first.
|
||||
|
||||
## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs.
|
||||
|
||||
output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example,
|
||||
and only output the json inside this tag, nothing else
|
||||
""",
|
||||
"FORMAT_EXAMPLE": '''
|
||||
{
|
||||
"Required Python third-party packages": [
|
||||
"flask==1.1.2",
|
||||
"bcrypt==3.2.0"
|
||||
],
|
||||
"Required Other language third-party packages": [
|
||||
"No third-party ..."
|
||||
],
|
||||
"Full API spec": """
|
||||
openapi: 3.0.0
|
||||
...
|
||||
description: A JSON object ...
|
||||
""",
|
||||
"Logic Analysis": [
|
||||
["game.py","Contains..."]
|
||||
],
|
||||
"Task list": [
|
||||
"game.py"
|
||||
],
|
||||
"Shared Knowledge": """
|
||||
'game.py' contains ...
|
||||
""",
|
||||
"Anything UNCLEAR": "We need ... how to start."
|
||||
}
|
||||
''',
|
||||
},
|
||||
"markdown": {
|
||||
"PROMPT_TEMPLATE": """
|
||||
# Context
|
||||
{context}
|
||||
|
||||
## Format example
|
||||
{format_example}
|
||||
-----
|
||||
Role: You are a project manager; the goal is to break down tasks according to PRD/technical design, give a task list, and analyze task dependencies to start with the prerequisite modules
|
||||
Requirements: Based on the context, fill in the following missing information, note that all sections are returned in Python code triple quote form seperatedly. Here the granularity of the task is a file, if there are any missing files, you can supplement them
|
||||
Attention: Use '##' to split sections, not '#', and '## <SECTION_NAME>' SHOULD WRITE BEFORE the code and triple quote.
|
||||
|
||||
## Required Python third-party packages: Provided in requirements.txt format
|
||||
|
||||
## Required Other language third-party packages: Provided in requirements.txt format
|
||||
|
||||
## Full API spec: Use OpenAPI 3.0. Describe all APIs that may be used by both frontend and backend.
|
||||
|
||||
## Logic Analysis: Provided as a Python list[list[str]. the first is filename, the second is class/method/function should be implemented in this file. Analyze the dependencies between the files, which work should be done first
|
||||
|
||||
## Task list: Provided as Python list[str]. Each str is a filename, the more at the beginning, the more it is a prerequisite dependency, should be done first
|
||||
|
||||
## Shared Knowledge: Anything that should be public like utils' functions, config's variables details that should make clear first.
|
||||
|
||||
## Anything UNCLEAR: Provide as Plain text. Make clear here. For example, don't forget a main entry. don't forget to init 3rd party libs.
|
||||
|
||||
""",
|
||||
"FORMAT_EXAMPLE": '''
|
||||
---
|
||||
## Required Python third-party packages
|
||||
```python
|
||||
"""
|
||||
flask==1.1.2
|
||||
bcrypt==3.2.0
|
||||
"""
|
||||
```
|
||||
|
||||
## Required Other language third-party packages
|
||||
```python
|
||||
"""
|
||||
No third-party ...
|
||||
"""
|
||||
```
|
||||
|
||||
## Full API spec
|
||||
```python
|
||||
"""
|
||||
openapi: 3.0.0
|
||||
...
|
||||
description: A JSON object ...
|
||||
"""
|
||||
```
|
||||
|
||||
## Logic Analysis
|
||||
```python
|
||||
[
|
||||
["game.py", "Contains ..."],
|
||||
]
|
||||
```
|
||||
|
||||
## Task list
|
||||
```python
|
||||
[
|
||||
"game.py",
|
||||
]
|
||||
```
|
||||
|
||||
## Shared Knowledge
|
||||
```python
|
||||
"""
|
||||
'game.py' contains ...
|
||||
"""
|
||||
```
|
||||
|
||||
## Anything UNCLEAR
|
||||
We need ... how to start.
|
||||
---
|
||||
''',
|
||||
},
|
||||
}
|
||||
OUTPUT_MAPPING = {
|
||||
"Required Python third-party packages": (List[str], ...),
|
||||
"Required Other language third-party packages": (List[str], ...),
|
||||
"Full API spec": (str, ...),
|
||||
"Logic Analysis": (List[List[str]], ...),
|
||||
"Task list": (List[str], ...),
|
||||
"Shared Knowledge": (str, ...),
|
||||
"Anything UNCLEAR": (str, ...),
|
||||
}
|
||||
|
||||
|
||||
class WriteTasks(Action):
|
||||
def __init__(self, name="CreateTasks", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
name: str = "CreateTasks"
|
||||
context: Optional[str] = None
|
||||
llm: BaseGPTAPI = Field(default_factory=LLM)
|
||||
|
||||
def _save(self, context, rsp):
|
||||
if context[-1].instruct_content:
|
||||
ws_name = context[-1].instruct_content.dict()["Python package name"]
|
||||
async def run(self, with_messages, schema=CONFIG.prompt_schema):
|
||||
system_design_file_repo = CONFIG.git_repo.new_file_repository(SYSTEM_DESIGN_FILE_REPO)
|
||||
changed_system_designs = system_design_file_repo.changed_files
|
||||
|
||||
tasks_file_repo = CONFIG.git_repo.new_file_repository(TASK_FILE_REPO)
|
||||
changed_tasks = tasks_file_repo.changed_files
|
||||
change_files = Documents()
|
||||
# Rewrite the system designs that have undergone changes based on the git head diff under
|
||||
# `docs/system_designs/`.
|
||||
for filename in changed_system_designs:
|
||||
task_doc = await self._update_tasks(
|
||||
filename=filename, system_design_file_repo=system_design_file_repo, tasks_file_repo=tasks_file_repo
|
||||
)
|
||||
change_files.docs[filename] = task_doc
|
||||
|
||||
# Rewrite the task files that have undergone changes based on the git head diff under `docs/tasks/`.
|
||||
for filename in changed_tasks:
|
||||
if filename in change_files.docs:
|
||||
continue
|
||||
task_doc = await self._update_tasks(
|
||||
filename=filename, system_design_file_repo=system_design_file_repo, tasks_file_repo=tasks_file_repo
|
||||
)
|
||||
change_files.docs[filename] = task_doc
|
||||
|
||||
if not change_files.docs:
|
||||
logger.info("Nothing has changed.")
|
||||
# Wait until all files under `docs/tasks/` are processed before sending the publish_message, leaving room for
|
||||
# global optimization in subsequent steps.
|
||||
return ActionOutput(content=change_files.json(), instruct_content=change_files)
|
||||
|
||||
async def _update_tasks(self, filename, system_design_file_repo, tasks_file_repo):
|
||||
system_design_doc = await system_design_file_repo.get(filename)
|
||||
task_doc = await tasks_file_repo.get(filename)
|
||||
if task_doc:
|
||||
task_doc = await self._merge(system_design_doc=system_design_doc, task_doc=task_doc)
|
||||
else:
|
||||
ws_name = CodeParser.parse_str(block="Python package name", text=context[-1].content)
|
||||
file_path = WORKSPACE_ROOT / ws_name / "docs/api_spec_and_tasks.md"
|
||||
file_path.write_text(json_to_markdown(rsp.instruct_content.dict()))
|
||||
rsp = await self._run_new_tasks(context=system_design_doc.content)
|
||||
task_doc = Document(
|
||||
root_path=TASK_FILE_REPO, filename=filename, content=rsp.instruct_content.json(ensure_ascii=False)
|
||||
)
|
||||
await tasks_file_repo.save(
|
||||
filename=filename, content=task_doc.content, dependencies={system_design_doc.root_relative_path}
|
||||
)
|
||||
await self._update_requirements(task_doc)
|
||||
await self._save_pdf(task_doc=task_doc)
|
||||
return task_doc
|
||||
|
||||
# Write requirements.txt
|
||||
requirements_path = WORKSPACE_ROOT / ws_name / "requirements.txt"
|
||||
requirements_path.write_text("\n".join(rsp.instruct_content.dict().get("Required Python third-party packages")))
|
||||
async def _run_new_tasks(self, context, schema=CONFIG.prompt_schema):
|
||||
node = await PM_NODE.fill(context, self.llm, schema)
|
||||
# prompt_template, format_example = get_template(templates, format)
|
||||
# prompt = prompt_template.format(context=context, format_example=format_example)
|
||||
# rsp = await self._aask_v1(prompt, "task", OUTPUT_MAPPING, format=format)
|
||||
return node
|
||||
|
||||
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)
|
||||
rsp = await self._aask_v1(prompt, "task", OUTPUT_MAPPING, format=format)
|
||||
self._save(context, rsp)
|
||||
return rsp
|
||||
async def _merge(self, system_design_doc, task_doc, schema=CONFIG.prompt_schema) -> Document:
|
||||
context = NEW_REQ_TEMPLATE.format(context=system_design_doc.content, old_tasks=task_doc.content)
|
||||
node = await PM_NODE.fill(context, self.llm, schema)
|
||||
task_doc.content = node.instruct_content.json(ensure_ascii=False)
|
||||
return task_doc
|
||||
|
||||
@staticmethod
|
||||
async def _update_requirements(doc):
|
||||
m = json.loads(doc.content)
|
||||
packages = set(m.get("Required Python third-party packages", set()))
|
||||
file_repo = CONFIG.git_repo.new_file_repository()
|
||||
requirement_doc = await file_repo.get(filename=PACKAGE_REQUIREMENTS_FILENAME)
|
||||
if not requirement_doc:
|
||||
requirement_doc = Document(filename=PACKAGE_REQUIREMENTS_FILENAME, root_path=".", content="")
|
||||
lines = requirement_doc.content.splitlines()
|
||||
for pkg in lines:
|
||||
if pkg == "":
|
||||
continue
|
||||
packages.add(pkg)
|
||||
await file_repo.save(PACKAGE_REQUIREMENTS_FILENAME, content="\n".join(packages))
|
||||
|
||||
@staticmethod
|
||||
async def _save_pdf(task_doc):
|
||||
await FileRepository.save_as(doc=task_doc, with_suffix=".md", relative_path=TASK_PDF_FILE_REPO)
|
||||
|
||||
|
||||
class AssignTasks(Action):
|
||||
|
|
|
|||
87
metagpt/actions/project_management_an.py
Normal file
87
metagpt/actions/project_management_an.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/12/14 15:28
|
||||
@Author : alexanderwu
|
||||
@File : project_management_an.py
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
from metagpt.actions.action_node import ActionNode
|
||||
from metagpt.logs import logger
|
||||
|
||||
REQUIRED_PYTHON_PACKAGES = ActionNode(
|
||||
key="Required Python packages",
|
||||
expected_type=List[str],
|
||||
instruction="Provide required Python packages in requirements.txt format.",
|
||||
example=["flask==1.1.2", "bcrypt==3.2.0"],
|
||||
)
|
||||
|
||||
REQUIRED_OTHER_LANGUAGE_PACKAGES = ActionNode(
|
||||
key="Required Other language third-party packages",
|
||||
expected_type=List[str],
|
||||
instruction="List down the required packages for languages other than Python.",
|
||||
example=["No third-party dependencies required"],
|
||||
)
|
||||
|
||||
LOGIC_ANALYSIS = ActionNode(
|
||||
key="Logic Analysis",
|
||||
expected_type=List[List[str]],
|
||||
instruction="Provide a list of files with the classes/methods/functions to be implemented, "
|
||||
"including dependency analysis and imports.",
|
||||
example=[
|
||||
["game.py", "Contains Game class and ... functions"],
|
||||
["main.py", "Contains main function, from game import Game"],
|
||||
],
|
||||
)
|
||||
|
||||
TASK_LIST = ActionNode(
|
||||
key="Task list",
|
||||
expected_type=List[str],
|
||||
instruction="Break down the tasks into a list of filenames, prioritized by dependency order.",
|
||||
example=["game.py", "main.py"],
|
||||
)
|
||||
|
||||
FULL_API_SPEC = ActionNode(
|
||||
key="Full API spec",
|
||||
expected_type=str,
|
||||
instruction="Describe all APIs using OpenAPI 3.0 spec that may be used by both frontend and backend. If front-end "
|
||||
"and back-end communication is not required, leave it blank.",
|
||||
example="openapi: 3.0.0 ...",
|
||||
)
|
||||
|
||||
SHARED_KNOWLEDGE = ActionNode(
|
||||
key="Shared Knowledge",
|
||||
expected_type=str,
|
||||
instruction="Detail any shared knowledge, like common utility functions or configuration variables.",
|
||||
example="'game.py' contains functions shared across the project.",
|
||||
)
|
||||
|
||||
ANYTHING_UNCLEAR_PM = ActionNode(
|
||||
key="Anything UNCLEAR",
|
||||
expected_type=str,
|
||||
instruction="Mention any unclear aspects in the project management context and try to clarify them.",
|
||||
example="Clarification needed on how to start and initialize third-party libraries.",
|
||||
)
|
||||
|
||||
NODES = [
|
||||
REQUIRED_PYTHON_PACKAGES,
|
||||
REQUIRED_OTHER_LANGUAGE_PACKAGES,
|
||||
LOGIC_ANALYSIS,
|
||||
TASK_LIST,
|
||||
FULL_API_SPEC,
|
||||
SHARED_KNOWLEDGE,
|
||||
ANYTHING_UNCLEAR_PM,
|
||||
]
|
||||
|
||||
|
||||
PM_NODE = ActionNode.from_children("PM_NODE", NODES)
|
||||
|
||||
|
||||
def main():
|
||||
prompt = PM_NODE.compile(context="")
|
||||
logger.info(prompt)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -3,7 +3,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Callable
|
||||
|
||||
from pydantic import parse_obj_as
|
||||
|
|
@ -49,7 +48,7 @@ based on the link credibility. If two results have equal credibility, prioritize
|
|||
ranked results' indices in JSON format, like [0, 1, 3, 4, ...], without including other words.
|
||||
"""
|
||||
|
||||
WEB_BROWSE_AND_SUMMARIZE_PROMPT = '''### Requirements
|
||||
WEB_BROWSE_AND_SUMMARIZE_PROMPT = """### Requirements
|
||||
1. Utilize the text in the "Reference Information" section to respond to the question "{query}".
|
||||
2. If the question cannot be directly answered using the text, but the text is related to the research topic, please provide \
|
||||
a comprehensive summary of the text.
|
||||
|
|
@ -58,10 +57,10 @@ a comprehensive summary of the text.
|
|||
|
||||
### Reference Information
|
||||
{content}
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
CONDUCT_RESEARCH_PROMPT = '''### Reference Information
|
||||
CONDUCT_RESEARCH_PROMPT = """### Reference Information
|
||||
{content}
|
||||
|
||||
### Requirements
|
||||
|
|
@ -73,11 +72,12 @@ above. The report must meet the following requirements:
|
|||
- Present data and findings in an intuitive manner, utilizing feature comparative tables, if applicable.
|
||||
- The report should have a minimum word count of 2,000 and be formatted with Markdown syntax following APA style guidelines.
|
||||
- Include all source URLs in APA format at the end of the report.
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
class CollectLinks(Action):
|
||||
"""Action class to collect links from a search engine."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "",
|
||||
|
|
@ -114,19 +114,24 @@ class CollectLinks(Action):
|
|||
keywords = OutputParser.extract_struct(keywords, list)
|
||||
keywords = parse_obj_as(list[str], keywords)
|
||||
except Exception as e:
|
||||
logger.exception(f"fail to get keywords related to the research topic \"{topic}\" for {e}")
|
||||
logger.exception(f"fail to get keywords related to the research topic '{topic}' for {e}")
|
||||
keywords = [topic]
|
||||
results = await asyncio.gather(*(self.search_engine.run(i, as_string=False) for i in keywords))
|
||||
|
||||
def gen_msg():
|
||||
while True:
|
||||
search_results = "\n".join(f"#### Keyword: {i}\n Search Result: {j}\n" for (i, j) in zip(keywords, results))
|
||||
prompt = SUMMARIZE_SEARCH_PROMPT.format(decomposition_nums=decomposition_nums, search_results=search_results)
|
||||
search_results = "\n".join(
|
||||
f"#### Keyword: {i}\n Search Result: {j}\n" for (i, j) in zip(keywords, results)
|
||||
)
|
||||
prompt = SUMMARIZE_SEARCH_PROMPT.format(
|
||||
decomposition_nums=decomposition_nums, search_results=search_results
|
||||
)
|
||||
yield prompt
|
||||
remove = max(results, key=len)
|
||||
remove.pop()
|
||||
if len(remove) == 0:
|
||||
break
|
||||
|
||||
prompt = reduce_message_length(gen_msg(), self.llm.model, system_text, CONFIG.max_tokens_rsp)
|
||||
logger.debug(prompt)
|
||||
queries = await self._aask(prompt, [system_text])
|
||||
|
|
@ -172,6 +177,7 @@ class CollectLinks(Action):
|
|||
|
||||
class WebBrowseAndSummarize(Action):
|
||||
"""Action class to explore the web and provide summaries of articles and webpages."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
|
|
@ -214,7 +220,9 @@ class WebBrowseAndSummarize(Action):
|
|||
for u, content in zip([url, *urls], contents):
|
||||
content = content.inner_text
|
||||
chunk_summaries = []
|
||||
for prompt in generate_prompt_chunk(content, prompt_template, self.llm.model, system_text, CONFIG.max_tokens_rsp):
|
||||
for prompt in generate_prompt_chunk(
|
||||
content, prompt_template, self.llm.model, system_text, CONFIG.max_tokens_rsp
|
||||
):
|
||||
logger.debug(prompt)
|
||||
summary = await self._aask(prompt, [system_text])
|
||||
if summary == "Not relevant.":
|
||||
|
|
@ -238,6 +246,7 @@ class WebBrowseAndSummarize(Action):
|
|||
|
||||
class ConductResearch(Action):
|
||||
"""Action class to conduct research and generate a research report."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if CONFIG.model_for_researcher_report:
|
||||
|
|
|
|||
|
|
@ -4,14 +4,28 @@
|
|||
@Time : 2023/5/11 17:46
|
||||
@Author : alexanderwu
|
||||
@File : run_code.py
|
||||
@Modified By: mashenquan, 2023/11/27.
|
||||
1. Mark the location of Console logs in the PROMPT_TEMPLATE with markdown code-block formatting to enhance
|
||||
the understanding for the LLM.
|
||||
2. Fix bug: Add the "install dependency" operation.
|
||||
3. Encapsulate the input of RunCode into RunCodeContext and encapsulate the output of RunCode into
|
||||
RunCodeResult to standardize and unify parameter passing between WriteCode, RunCode, and DebugError.
|
||||
4. According to section 2.2.3.5.7 of RFC 135, change the method of transferring file content
|
||||
(code files, unit test files, log files) from using the message to using the file name.
|
||||
5. Merged the `Config` class of send18:dev branch to take over the set/get operations of the Environment
|
||||
class.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import traceback
|
||||
from typing import Tuple
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from metagpt.actions.action import Action
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.llm import LLM, BaseGPTAPI
|
||||
from metagpt.logs import logger
|
||||
from metagpt.schema import RunCodeContext, RunCodeResult
|
||||
from metagpt.utils.exceptions import handle_exception
|
||||
|
||||
PROMPT_TEMPLATE = """
|
||||
Role: You are a senior development and qa engineer, your role is summarize the code running result.
|
||||
|
|
@ -51,25 +65,29 @@ CONTEXT = """
|
|||
## Running Command
|
||||
{command}
|
||||
## Running Output
|
||||
standard output: {outs};
|
||||
standard errors: {errs};
|
||||
standard output:
|
||||
```text
|
||||
{outs}
|
||||
```
|
||||
standard errors:
|
||||
```text
|
||||
{errs}
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
class RunCode(Action):
|
||||
def __init__(self, name="RunCode", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
name: str = "RunCode"
|
||||
context: RunCodeContext = Field(default_factory=RunCodeContext)
|
||||
llm: BaseGPTAPI = Field(default_factory=LLM)
|
||||
|
||||
@classmethod
|
||||
@handle_exception
|
||||
async def run_text(cls, code) -> Tuple[str, str]:
|
||||
try:
|
||||
# We will document_store the result in this dictionary
|
||||
namespace = {}
|
||||
exec(code, namespace)
|
||||
return namespace.get("result", ""), ""
|
||||
except Exception:
|
||||
# If there is an error in the code, return the error message
|
||||
return "", traceback.format_exc()
|
||||
# We will document_store the result in this dictionary
|
||||
namespace = {}
|
||||
exec(code, namespace)
|
||||
return namespace.get("result", ""), ""
|
||||
|
||||
@classmethod
|
||||
async def run_script(cls, working_directory, additional_python_paths=[], command=[]) -> Tuple[str, str]:
|
||||
|
|
@ -77,17 +95,19 @@ class RunCode(Action):
|
|||
additional_python_paths = [str(path) for path in additional_python_paths]
|
||||
|
||||
# Copy the current environment variables
|
||||
env = os.environ.copy()
|
||||
env = CONFIG.new_environ()
|
||||
|
||||
# Modify the PYTHONPATH environment variable
|
||||
additional_python_paths = [working_directory] + additional_python_paths
|
||||
additional_python_paths = ":".join(additional_python_paths)
|
||||
env["PYTHONPATH"] = additional_python_paths + ":" + env.get("PYTHONPATH", "")
|
||||
RunCode._install_dependencies(working_directory=working_directory, env=env)
|
||||
|
||||
# Start the subprocess
|
||||
process = subprocess.Popen(
|
||||
command, cwd=working_directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env
|
||||
)
|
||||
logger.info(" ".join(command))
|
||||
|
||||
try:
|
||||
# Wait for the process to complete, with a timeout
|
||||
|
|
@ -98,31 +118,45 @@ class RunCode(Action):
|
|||
stdout, stderr = process.communicate()
|
||||
return stdout.decode("utf-8"), stderr.decode("utf-8")
|
||||
|
||||
async def run(
|
||||
self, code, mode="script", code_file_name="", test_code="", test_file_name="", command=[], **kwargs
|
||||
) -> str:
|
||||
logger.info(f"Running {' '.join(command)}")
|
||||
if mode == "script":
|
||||
outs, errs = await self.run_script(command=command, **kwargs)
|
||||
elif mode == "text":
|
||||
outs, errs = await self.run_text(code=code)
|
||||
async def run(self, *args, **kwargs) -> RunCodeResult:
|
||||
logger.info(f"Running {' '.join(self.context.command)}")
|
||||
if self.context.mode == "script":
|
||||
outs, errs = await self.run_script(
|
||||
command=self.context.command,
|
||||
working_directory=self.context.working_directory,
|
||||
additional_python_paths=self.context.additional_python_paths,
|
||||
)
|
||||
elif self.context.mode == "text":
|
||||
outs, errs = await self.run_text(code=self.context.code)
|
||||
|
||||
logger.info(f"{outs=}")
|
||||
logger.info(f"{errs=}")
|
||||
|
||||
context = CONTEXT.format(
|
||||
code=code,
|
||||
code_file_name=code_file_name,
|
||||
test_code=test_code,
|
||||
test_file_name=test_file_name,
|
||||
command=" ".join(command),
|
||||
code=self.context.code,
|
||||
code_file_name=self.context.code_filename,
|
||||
test_code=self.context.test_code,
|
||||
test_file_name=self.context.test_filename,
|
||||
command=" ".join(self.context.command),
|
||||
outs=outs[:500], # outs might be long but they are not important, truncate them to avoid token overflow
|
||||
errs=errs[:10000], # truncate errors to avoid token overflow
|
||||
)
|
||||
|
||||
prompt = PROMPT_TEMPLATE.format(context=context)
|
||||
rsp = await self._aask(prompt)
|
||||
return RunCodeResult(summary=rsp, stdout=outs, stderr=errs)
|
||||
|
||||
result = context + rsp
|
||||
@staticmethod
|
||||
@handle_exception(exception_type=subprocess.CalledProcessError)
|
||||
def _install_via_subprocess(cmd, check, cwd, env):
|
||||
return subprocess.run(cmd, check=check, cwd=cwd, env=env)
|
||||
|
||||
return result
|
||||
@staticmethod
|
||||
def _install_dependencies(working_directory, env):
|
||||
install_command = ["python", "-m", "pip", "install", "-r", "requirements.txt"]
|
||||
logger.info(" ".join(install_command))
|
||||
RunCode._install_via_subprocess(install_command, check=True, cwd=working_directory, env=env)
|
||||
|
||||
install_pytest_command = ["python", "-m", "pip", "install", "pytest"]
|
||||
logger.info(" ".join(install_pytest_command))
|
||||
RunCode._install_via_subprocess(install_pytest_command, check=True, cwd=working_directory, env=env)
|
||||
|
|
|
|||
|
|
@ -5,11 +5,16 @@
|
|||
@Author : alexanderwu
|
||||
@File : search_google.py
|
||||
"""
|
||||
from typing import Optional
|
||||
|
||||
import pydantic
|
||||
from pydantic import Field, root_validator
|
||||
|
||||
from metagpt.actions import Action
|
||||
from metagpt.config import Config
|
||||
from metagpt.config import CONFIG, Config
|
||||
from metagpt.llm import LLM
|
||||
from metagpt.logs import logger
|
||||
from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
from metagpt.schema import Message
|
||||
from metagpt.tools.search_engine import SearchEngine
|
||||
|
||||
|
|
@ -54,7 +59,6 @@ SEARCH_AND_SUMMARIZE_PROMPT = """
|
|||
|
||||
"""
|
||||
|
||||
|
||||
SEARCH_AND_SUMMARIZE_SALES_SYSTEM = """## Requirements
|
||||
1. Please summarize the latest dialogue based on the reference information (secondary) and dialogue history (primary). Do not include text that is irrelevant to the conversation.
|
||||
- The context is for reference only. If it is irrelevant to the user's search request history, please reduce its reference and usage.
|
||||
|
|
@ -101,17 +105,31 @@ You are a member of a professional butler team and will provide helpful suggesti
|
|||
|
||||
|
||||
class SearchAndSummarize(Action):
|
||||
def __init__(self, name="", context=None, llm=None, engine=None, search_func=None):
|
||||
self.config = Config()
|
||||
self.engine = engine or self.config.search_engine
|
||||
name: str = ""
|
||||
content: Optional[str] = None
|
||||
llm: BaseGPTAPI = Field(default_factory=LLM)
|
||||
config: None = Field(default_factory=Config)
|
||||
engine: Optional[str] = CONFIG.search_engine
|
||||
search_func: Optional[str] = None
|
||||
search_engine: SearchEngine = None
|
||||
|
||||
result = ""
|
||||
|
||||
@root_validator
|
||||
def validate_engine_and_run_func(cls, values):
|
||||
engine = values.get("engine")
|
||||
search_func = values.get("search_func")
|
||||
config = Config()
|
||||
|
||||
if engine is None:
|
||||
engine = config.search_engine
|
||||
try:
|
||||
self.search_engine = SearchEngine(self.engine, run_func=search_func)
|
||||
search_engine = SearchEngine(engine=engine, run_func=search_func)
|
||||
except pydantic.ValidationError:
|
||||
self.search_engine = None
|
||||
search_engine = None
|
||||
|
||||
self.result = ""
|
||||
super().__init__(name, context, llm)
|
||||
values["search_engine"] = search_engine
|
||||
return values
|
||||
|
||||
async def run(self, context: list[Message], system_text=SEARCH_AND_SUMMARIZE_SYSTEM) -> str:
|
||||
if self.search_engine is None:
|
||||
|
|
@ -130,8 +148,7 @@ class SearchAndSummarize(Action):
|
|||
system_prompt = [system_text]
|
||||
|
||||
prompt = SEARCH_AND_SUMMARIZE_PROMPT.format(
|
||||
# PREFIX = self.prefix,
|
||||
ROLE=self.profile,
|
||||
ROLE=self.prefix,
|
||||
CONTEXT=rsp,
|
||||
QUERY_HISTORY="\n".join([str(i) for i in context[:-1]]),
|
||||
QUERY=str(context[-1]),
|
||||
|
|
@ -140,4 +157,3 @@ class SearchAndSummarize(Action):
|
|||
logger.debug(prompt)
|
||||
logger.debug(result)
|
||||
return result
|
||||
|
||||
124
metagpt/actions/summarize_code.py
Normal file
124
metagpt/actions/summarize_code.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Author : alexanderwu
|
||||
@File : summarize_code.py
|
||||
@Modified By: mashenquan, 2023/12/5. Archive the summarization content of issue discovery for use in WriteCode.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import Field
|
||||
from tenacity import retry, stop_after_attempt, wait_random_exponential
|
||||
|
||||
from metagpt.actions.action import Action
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.const import SYSTEM_DESIGN_FILE_REPO, TASK_FILE_REPO
|
||||
from metagpt.llm import LLM, BaseGPTAPI
|
||||
from metagpt.logs import logger
|
||||
from metagpt.schema import CodeSummarizeContext
|
||||
from metagpt.utils.file_repository import FileRepository
|
||||
|
||||
PROMPT_TEMPLATE = """
|
||||
NOTICE
|
||||
Role: You are a professional software engineer, and your main task is to review the code.
|
||||
Language: Please use the same language as the user requirement, but the title and code should be still in English. For example, if the user speaks Chinese, the specific text of your answer should also be in Chinese.
|
||||
ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example".
|
||||
|
||||
-----
|
||||
# System Design
|
||||
```text
|
||||
{system_design}
|
||||
```
|
||||
-----
|
||||
# Tasks
|
||||
```text
|
||||
{tasks}
|
||||
```
|
||||
-----
|
||||
{code_blocks}
|
||||
|
||||
## Code Review All: Please read all historical files and find possible bugs in the files, such as unimplemented functions, calling errors, unreferences, etc.
|
||||
|
||||
## Call flow: mermaid code, based on the implemented function, use mermaid to draw a complete call chain
|
||||
|
||||
## Summary: Summary based on the implementation of historical files
|
||||
|
||||
## TODOs: Python dict[str, str], write down the list of files that need to be modified and the reasons. We will modify them later.
|
||||
|
||||
"""
|
||||
|
||||
FORMAT_EXAMPLE = """
|
||||
|
||||
## Code Review All
|
||||
|
||||
### a.py
|
||||
- It fulfills less of xxx requirements...
|
||||
- Field yyy is not given...
|
||||
-...
|
||||
|
||||
### b.py
|
||||
...
|
||||
|
||||
### c.py
|
||||
...
|
||||
|
||||
## Call flow
|
||||
```mermaid
|
||||
flowchart TB
|
||||
c1-->a2
|
||||
subgraph one
|
||||
a1-->a2
|
||||
end
|
||||
subgraph two
|
||||
b1-->b2
|
||||
end
|
||||
subgraph three
|
||||
c1-->c2
|
||||
end
|
||||
```
|
||||
|
||||
## Summary
|
||||
- a.py:...
|
||||
- b.py:...
|
||||
- c.py:...
|
||||
- ...
|
||||
|
||||
## TODOs
|
||||
{
|
||||
"a.py": "implement requirement xxx...",
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class SummarizeCode(Action):
|
||||
name: str = "SummarizeCode"
|
||||
context: CodeSummarizeContext = Field(default_factory=CodeSummarizeContext)
|
||||
llm: BaseGPTAPI = Field(default_factory=LLM)
|
||||
|
||||
@retry(stop=stop_after_attempt(2), wait=wait_random_exponential(min=1, max=60))
|
||||
async def summarize_code(self, prompt):
|
||||
code_rsp = await self._aask(prompt)
|
||||
return code_rsp
|
||||
|
||||
async def run(self):
|
||||
design_pathname = Path(self.context.design_filename)
|
||||
design_doc = await FileRepository.get_file(filename=design_pathname.name, relative_path=SYSTEM_DESIGN_FILE_REPO)
|
||||
task_pathname = Path(self.context.task_filename)
|
||||
task_doc = await FileRepository.get_file(filename=task_pathname.name, relative_path=TASK_FILE_REPO)
|
||||
src_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.src_workspace)
|
||||
code_blocks = []
|
||||
for filename in self.context.codes_filenames:
|
||||
code_doc = await src_file_repo.get(filename)
|
||||
code_block = f"```python\n{code_doc.content}\n```\n-----"
|
||||
code_blocks.append(code_block)
|
||||
format_example = FORMAT_EXAMPLE
|
||||
prompt = PROMPT_TEMPLATE.format(
|
||||
system_design=design_doc.content,
|
||||
tasks=task_doc.content,
|
||||
code_blocks="\n".join(code_blocks),
|
||||
format_example=format_example,
|
||||
)
|
||||
logger.info("Summarize code..")
|
||||
rsp = await self.summarize_code(prompt)
|
||||
return rsp
|
||||
|
|
@ -4,79 +4,154 @@
|
|||
@Time : 2023/5/11 17:45
|
||||
@Author : alexanderwu
|
||||
@File : write_code.py
|
||||
@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.1.3 of RFC 116, modify the data type of the `cause_by`
|
||||
value of the `Message` object.
|
||||
@Modified By: mashenquan, 2023-11-27.
|
||||
1. Mark the location of Design, Tasks, Legacy Code and Debug logs in the PROMPT_TEMPLATE with markdown
|
||||
code-block formatting to enhance the understanding for the LLM.
|
||||
2. Following the think-act principle, solidify the task parameters when creating the WriteCode object, rather
|
||||
than passing them in when calling the run function.
|
||||
3. Encapsulate the input of RunCode into RunCodeContext and encapsulate the output of RunCode into
|
||||
RunCodeResult to standardize and unify parameter passing between WriteCode, RunCode, and DebugError.
|
||||
"""
|
||||
from metagpt.actions import WriteDesign
|
||||
|
||||
import json
|
||||
|
||||
from pydantic import Field
|
||||
from tenacity import retry, stop_after_attempt, wait_random_exponential
|
||||
|
||||
from metagpt.actions.action import Action
|
||||
from metagpt.const import WORKSPACE_ROOT
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.const import (
|
||||
BUGFIX_FILENAME,
|
||||
CODE_SUMMARIES_FILE_REPO,
|
||||
DOCS_FILE_REPO,
|
||||
TASK_FILE_REPO,
|
||||
TEST_OUTPUTS_FILE_REPO,
|
||||
)
|
||||
from metagpt.llm import LLM
|
||||
from metagpt.logs import logger
|
||||
from metagpt.schema import Message
|
||||
from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
from metagpt.schema import CodingContext, Document, RunCodeResult
|
||||
from metagpt.utils.common import CodeParser
|
||||
from tenacity import retry, stop_after_attempt, wait_fixed
|
||||
from metagpt.utils.file_repository import FileRepository
|
||||
|
||||
PROMPT_TEMPLATE = """
|
||||
NOTICE
|
||||
Role: You are a professional engineer; the main goal is to write PEP8 compliant, elegant, modular, easy to read and maintain Python 3.9 code (but you can also use other programming language)
|
||||
Role: You are a professional engineer; the main goal is to write google-style, elegant, modular, easy to read and maintain code
|
||||
Language: Please use the same language as the user requirement, but the title and code should be still in English. For example, if the user speaks Chinese, the specific text of your answer should also be in Chinese.
|
||||
ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example".
|
||||
|
||||
## Code: {filename} Write code with triple quoto, based on the following list and context.
|
||||
1. Do your best to implement THIS ONLY ONE FILE. ONLY USE EXISTING API. IF NO API, IMPLEMENT IT.
|
||||
2. Requirement: Based on the context, implement one following code file, note to return only in code form, your code will be part of the entire project, so please implement complete, reliable, reusable code snippets
|
||||
3. Attention1: If there is any setting, ALWAYS SET A DEFAULT VALUE, ALWAYS USE STRONG TYPE AND EXPLICIT VARIABLE.
|
||||
4. Attention2: YOU MUST FOLLOW "Data structures and interface definitions". DONT CHANGE ANY DESIGN.
|
||||
5. Think before writing: What should be implemented and provided in this document?
|
||||
6. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE.
|
||||
7. Do not use public member functions that do not exist in your design.
|
||||
|
||||
-----
|
||||
# Context
|
||||
{context}
|
||||
-----
|
||||
## Format example
|
||||
-----
|
||||
## Design
|
||||
{design}
|
||||
|
||||
## Tasks
|
||||
{tasks}
|
||||
|
||||
## Legacy Code
|
||||
```Code
|
||||
{code}
|
||||
```
|
||||
|
||||
## Debug logs
|
||||
```text
|
||||
{logs}
|
||||
|
||||
{summary_log}
|
||||
```
|
||||
|
||||
## Bug Feedback logs
|
||||
```text
|
||||
{feedback}
|
||||
```
|
||||
|
||||
# Format example
|
||||
## Code: {filename}
|
||||
```python
|
||||
## {filename}
|
||||
...
|
||||
```
|
||||
-----
|
||||
|
||||
# Instruction: Based on the context, follow "Format example", write code.
|
||||
|
||||
## Code: {filename}. Write code with triple quoto, based on the following attentions and context.
|
||||
1. Only One file: do your best to implement THIS ONLY ONE FILE.
|
||||
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. CAREFULLY CHECK THAT YOU DONT MISS ANY NECESSARY CLASS/FUNCTION IN THIS FILE.
|
||||
6. Before using a external variable/module, make sure you import it first.
|
||||
7. Write out EVERY CODE DETAIL, DON'T LEAVE TODO.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class WriteCode(Action):
|
||||
def __init__(self, name="WriteCode", context: list[Message] = None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
name: str = "WriteCode"
|
||||
context: Document = Field(default_factory=Document)
|
||||
llm: BaseGPTAPI = Field(default_factory=LLM)
|
||||
|
||||
def _is_invalid(self, filename):
|
||||
return any(i in filename for i in ["mp3", "wav"])
|
||||
|
||||
def _save(self, context, filename, code):
|
||||
# logger.info(filename)
|
||||
# logger.info(code_rsp)
|
||||
if self._is_invalid(filename):
|
||||
return
|
||||
|
||||
design = [i for i in context if i.cause_by == WriteDesign][0]
|
||||
|
||||
ws_name = CodeParser.parse_str(block="Python package name", text=design.content)
|
||||
ws_path = WORKSPACE_ROOT / ws_name
|
||||
if f"{ws_name}/" not in filename and all(i not in filename for i in ["requirements.txt", ".md"]):
|
||||
ws_path = ws_path / ws_name
|
||||
code_path = ws_path / filename
|
||||
code_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
code_path.write_text(code)
|
||||
logger.info(f"Saving Code to {code_path}")
|
||||
|
||||
@retry(stop=stop_after_attempt(2), wait=wait_fixed(1))
|
||||
async def write_code(self, prompt):
|
||||
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
|
||||
async def write_code(self, prompt) -> str:
|
||||
code_rsp = await self._aask(prompt)
|
||||
code = CodeParser.parse_code(block="", text=code_rsp)
|
||||
return code
|
||||
|
||||
async def run(self, context, filename):
|
||||
prompt = PROMPT_TEMPLATE.format(context=context, filename=filename)
|
||||
logger.info(f'Writing {filename}..')
|
||||
async def run(self, *args, **kwargs) -> CodingContext:
|
||||
bug_feedback = await FileRepository.get_file(filename=BUGFIX_FILENAME, relative_path=DOCS_FILE_REPO)
|
||||
coding_context = CodingContext.loads(self.context.content)
|
||||
test_doc = await FileRepository.get_file(
|
||||
filename="test_" + coding_context.filename + ".json", relative_path=TEST_OUTPUTS_FILE_REPO
|
||||
)
|
||||
summary_doc = None
|
||||
if coding_context.design_doc and coding_context.design_doc.filename:
|
||||
summary_doc = await FileRepository.get_file(
|
||||
filename=coding_context.design_doc.filename, relative_path=CODE_SUMMARIES_FILE_REPO
|
||||
)
|
||||
logs = ""
|
||||
if test_doc:
|
||||
test_detail = RunCodeResult.loads(test_doc.content)
|
||||
logs = test_detail.stderr
|
||||
|
||||
if bug_feedback:
|
||||
code_context = coding_context.code_doc.content
|
||||
else:
|
||||
code_context = await self.get_codes(coding_context.task_doc, exclude=self.context.filename)
|
||||
|
||||
prompt = PROMPT_TEMPLATE.format(
|
||||
design=coding_context.design_doc.content if coding_context.design_doc else "",
|
||||
tasks=coding_context.task_doc.content if coding_context.task_doc else "",
|
||||
code=code_context,
|
||||
logs=logs,
|
||||
feedback=bug_feedback.content if bug_feedback else "",
|
||||
filename=self.context.filename,
|
||||
summary_log=summary_doc.content if summary_doc else "",
|
||||
)
|
||||
logger.info(f"Writing {coding_context.filename}..")
|
||||
code = await self.write_code(prompt)
|
||||
# code_rsp = await self._aask_v1(prompt, "code_rsp", OUTPUT_MAPPING)
|
||||
# self._save(context, filename, code)
|
||||
return code
|
||||
|
||||
if not coding_context.code_doc:
|
||||
# avoid root_path pydantic ValidationError if use WriteCode alone
|
||||
root_path = CONFIG.src_workspace if CONFIG.src_workspace else ""
|
||||
coding_context.code_doc = Document(filename=coding_context.filename, root_path=root_path)
|
||||
coding_context.code_doc.content = code
|
||||
return coding_context
|
||||
|
||||
@staticmethod
|
||||
async def get_codes(task_doc, exclude) -> str:
|
||||
if not task_doc:
|
||||
return ""
|
||||
if not task_doc.content:
|
||||
task_doc.content = FileRepository.get_file(filename=task_doc.filename, relative_path=TASK_FILE_REPO)
|
||||
m = json.loads(task_doc.content)
|
||||
code_filenames = m.get("Task list", [])
|
||||
codes = []
|
||||
src_file_repo = CONFIG.git_repo.new_file_repository(relative_path=CONFIG.src_workspace)
|
||||
for filename in code_filenames:
|
||||
if filename == exclude:
|
||||
continue
|
||||
doc = await src_file_repo.get(filename=filename)
|
||||
if not doc:
|
||||
continue
|
||||
codes.append(f"----- {filename}\n" + doc.content)
|
||||
return "\n".join(codes)
|
||||
|
|
|
|||
591
metagpt/actions/write_code_an_draft.py
Normal file
591
metagpt/actions/write_code_an_draft.py
Normal file
|
|
@ -0,0 +1,591 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Author : alexanderwu
|
||||
@File : write_review.py
|
||||
"""
|
||||
import asyncio
|
||||
from typing import List
|
||||
|
||||
from metagpt.actions import Action
|
||||
from metagpt.actions.action_node import ActionNode
|
||||
|
||||
REVIEW = ActionNode(
|
||||
key="Review",
|
||||
expected_type=List[str],
|
||||
instruction="Act as an experienced reviewer and critically assess the given output. Provide specific and"
|
||||
" constructive feedback, highlighting areas for improvement and suggesting changes.",
|
||||
example=[
|
||||
"The logic in the function `calculate_total` seems flawed. Shouldn't it consider the discount rate as well?",
|
||||
"The TODO function is not implemented yet? Should we implement it before commit?",
|
||||
],
|
||||
)
|
||||
|
||||
LGTM = ActionNode(
|
||||
key="LGTM",
|
||||
expected_type=str,
|
||||
instruction="LGTM/LBTM. If the code is fully implemented, "
|
||||
"give a LGTM (Looks Good To Me), otherwise provide a LBTM (Looks Bad To Me).",
|
||||
example="LBTM",
|
||||
)
|
||||
|
||||
ACTIONS = ActionNode(
|
||||
key="Actions",
|
||||
expected_type=str,
|
||||
instruction="Based on the code review outcome, suggest actionable steps. This can include code changes, "
|
||||
"refactoring suggestions, or any follow-up tasks.",
|
||||
example="""1. Refactor the `process_data` method to improve readability and efficiency.
|
||||
2. Cover edge cases in the `validate_user` function.
|
||||
3. Implement a the TODO in the `calculate_total` function.
|
||||
4. Fix the `handle_events` method to update the game state only if a move is successful.
|
||||
```python
|
||||
def handle_events(self):
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
return False
|
||||
if event.type == pygame.KEYDOWN:
|
||||
moved = False
|
||||
if event.key == pygame.K_UP:
|
||||
moved = self.game.move('UP')
|
||||
elif event.key == pygame.K_DOWN:
|
||||
moved = self.game.move('DOWN')
|
||||
elif event.key == pygame.K_LEFT:
|
||||
moved = self.game.move('LEFT')
|
||||
elif event.key == pygame.K_RIGHT:
|
||||
moved = self.game.move('RIGHT')
|
||||
if moved:
|
||||
# Update the game state only if a move was successful
|
||||
self.render()
|
||||
return True
|
||||
```
|
||||
""",
|
||||
)
|
||||
|
||||
WRITE_DRAFT = ActionNode(
|
||||
key="WriteDraft",
|
||||
expected_type=str,
|
||||
instruction="Could you write draft code for move function in order to implement it?",
|
||||
example="Draft: ...",
|
||||
)
|
||||
|
||||
|
||||
WRITE_MOVE_FUNCTION = ActionNode(
|
||||
key="WriteFunction",
|
||||
expected_type=str,
|
||||
instruction="write code for the function not implemented.",
|
||||
example="""
|
||||
```Code
|
||||
...
|
||||
```
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
REWRITE_CODE = ActionNode(
|
||||
key="RewriteCode",
|
||||
expected_type=str,
|
||||
instruction="""rewrite code based on the Review and Actions""",
|
||||
example="""
|
||||
```python
|
||||
## example.py
|
||||
def calculate_total(price, quantity):
|
||||
total = price * quantity
|
||||
```
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
CODE_REVIEW_CONTEXT = """
|
||||
# System
|
||||
Role: You are a professional software engineer, and your main task is to review and revise the code. You need to ensure that the code conforms to the google-style standards, is elegantly designed and modularized, easy to read and maintain.
|
||||
Language: Please use the same language as the user requirement, but the title and code should be still in English. For example, if the user speaks Chinese, the specific text of your answer should also be in Chinese.
|
||||
|
||||
# Context
|
||||
## System Design
|
||||
{"Implementation approach": "我们将使用HTML、CSS和JavaScript来实现这个单机的响应式2048游戏。为了确保游戏性能流畅和响应式设计,我们会选择使用Vue.js框架,因为它易于上手且适合构建交互式界面。我们还将使用localStorage来记录玩家的最高分。", "File list": ["index.html", "styles.css", "main.js", "game.js", "storage.js"], "Data structures and interfaces": "classDiagram\
|
||||
class Game {\
|
||||
-board Array\
|
||||
-score Number\
|
||||
-bestScore Number\
|
||||
+constructor()\
|
||||
+startGame()\
|
||||
+move(direction: String)\
|
||||
+getBoard() Array\
|
||||
+getScore() Number\
|
||||
+getBestScore() Number\
|
||||
+setBestScore(score: Number)\
|
||||
}\
|
||||
class Storage {\
|
||||
+getBestScore() Number\
|
||||
+setBestScore(score: Number)\
|
||||
}\
|
||||
class Main {\
|
||||
+init()\
|
||||
+bindEvents()\
|
||||
}\
|
||||
Game --> Storage : uses\
|
||||
Main --> Game : uses", "Program call flow": "sequenceDiagram\
|
||||
participant M as Main\
|
||||
participant G as Game\
|
||||
participant S as Storage\
|
||||
M->>G: init()\
|
||||
G->>S: getBestScore()\
|
||||
S-->>G: return bestScore\
|
||||
M->>G: bindEvents()\
|
||||
M->>G: startGame()\
|
||||
loop Game Loop\
|
||||
M->>G: move(direction)\
|
||||
G->>S: setBestScore(score)\
|
||||
S-->>G: return\
|
||||
end", "Anything UNCLEAR": "目前项目要求明确,没有不清楚的地方。"}
|
||||
|
||||
## Tasks
|
||||
{"Required Python packages": ["无需Python包"], "Required Other language third-party packages": ["vue.js"], "Logic Analysis": [["index.html", "作为游戏的入口文件和主要的HTML结构"], ["styles.css", "包含所有的CSS样式,确保游戏界面美观"], ["main.js", "包含Main类,负责初始化游戏和绑定事件"], ["game.js", "包含Game类,负责游戏逻辑,如开始游戏、移动方块等"], ["storage.js", "包含Storage类,用于获取和设置玩家的最高分"]], "Task list": ["index.html", "styles.css", "storage.js", "game.js", "main.js"], "Full API spec": "", "Shared Knowledge": "\'game.js\' 包含游戏逻辑相关的函数,被 \'main.js\' 调用。", "Anything UNCLEAR": "目前项目要求明确,没有不清楚的地方。"}
|
||||
|
||||
## Code Files
|
||||
----- index.html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>2048游戏</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1>2048</h1>
|
||||
<div class="scores-container">
|
||||
<div class="score-container">
|
||||
<div class="score-header">分数</div>
|
||||
<div>{{ score }}</div>
|
||||
</div>
|
||||
<div class="best-container">
|
||||
<div class="best-header">最高分</div>
|
||||
<div>{{ bestScore }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="game-container">
|
||||
<div v-for="(row, rowIndex) in board" :key="rowIndex" class="grid-row">
|
||||
<div v-for="(cell, cellIndex) in row" :key="cellIndex" class="grid-cell" :class="\'number-cell-\' + cell">
|
||||
{{ cell !== 0 ? cell : \'\' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="startGame" aria-label="开始新游戏">新游戏</button>
|
||||
</div>
|
||||
|
||||
<script src="storage.js"></script>
|
||||
<script src="game.js"></script>
|
||||
<script src="main.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
----- styles.css
|
||||
/* styles.css */
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: \'Arial\', sans-serif;
|
||||
}
|
||||
|
||||
#app {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
color: #776e65;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #776e65;
|
||||
font-size: 72px;
|
||||
font-weight: bold;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.scores-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.score-container, .best-container {
|
||||
background: #bbada0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin: 0 10px;
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.score-header, .best-header {
|
||||
color: #eee4da;
|
||||
font-size: 18px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
max-width: 500px;
|
||||
margin: 0 auto 20px;
|
||||
background: #bbada0;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.grid-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.grid-cell {
|
||||
background: #cdc1b4;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin: 5px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 35px;
|
||||
font-weight: bold;
|
||||
color: #776e65;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Dynamic classes for different number cells */
|
||||
.number-cell-2 {
|
||||
background: #eee4da;
|
||||
}
|
||||
|
||||
.number-cell-4 {
|
||||
background: #ede0c8;
|
||||
}
|
||||
|
||||
.number-cell-8 {
|
||||
background: #f2b179;
|
||||
color: #f9f6f2;
|
||||
}
|
||||
|
||||
.number-cell-16 {
|
||||
background: #f59563;
|
||||
color: #f9f6f2;
|
||||
}
|
||||
|
||||
.number-cell-32 {
|
||||
background: #f67c5f;
|
||||
color: #f9f6f2;
|
||||
}
|
||||
|
||||
.number-cell-64 {
|
||||
background: #f65e3b;
|
||||
color: #f9f6f2;
|
||||
}
|
||||
|
||||
.number-cell-128 {
|
||||
background: #edcf72;
|
||||
color: #f9f6f2;
|
||||
}
|
||||
|
||||
.number-cell-256 {
|
||||
background: #edcc61;
|
||||
color: #f9f6f2;
|
||||
}
|
||||
|
||||
.number-cell-512 {
|
||||
background: #edc850;
|
||||
color: #f9f6f2;
|
||||
}
|
||||
|
||||
.number-cell-1024 {
|
||||
background: #edc53f;
|
||||
color: #f9f6f2;
|
||||
}
|
||||
|
||||
.number-cell-2048 {
|
||||
background: #edc22e;
|
||||
color: #f9f6f2;
|
||||
}
|
||||
|
||||
/* Larger numbers need smaller font sizes */
|
||||
.number-cell-1024, .number-cell-2048 {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #8f7a66;
|
||||
color: #f9f6f2;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
padding: 10px 20px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #9f8b76;
|
||||
}
|
||||
|
||||
----- storage.js
|
||||
## storage.js
|
||||
class Storage {
|
||||
// 获取最高分
|
||||
getBestScore() {
|
||||
// 尝试从localStorage中获取最高分,如果不存在则默认为0
|
||||
const bestScore = localStorage.getItem(\'bestScore\');
|
||||
return bestScore ? Number(bestScore) : 0;
|
||||
}
|
||||
|
||||
// 设置最高分
|
||||
setBestScore(score) {
|
||||
// 将最高分设置到localStorage中
|
||||
localStorage.setItem(\'bestScore\', score.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
## Code to be Reviewed: game.js
|
||||
```Code
|
||||
## game.js
|
||||
class Game {
|
||||
constructor() {
|
||||
this.board = this.createEmptyBoard();
|
||||
this.score = 0;
|
||||
this.bestScore = 0;
|
||||
}
|
||||
|
||||
createEmptyBoard() {
|
||||
const board = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
board[i] = [0, 0, 0, 0];
|
||||
}
|
||||
return board;
|
||||
}
|
||||
|
||||
startGame() {
|
||||
this.board = this.createEmptyBoard();
|
||||
this.score = 0;
|
||||
this.addRandomTile();
|
||||
this.addRandomTile();
|
||||
}
|
||||
|
||||
addRandomTile() {
|
||||
let emptyCells = [];
|
||||
for (let r = 0; r < 4; r++) {
|
||||
for (let c = 0; c < 4; c++) {
|
||||
if (this.board[r][c] === 0) {
|
||||
emptyCells.push({ r, c });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (emptyCells.length > 0) {
|
||||
let randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
|
||||
this.board[randomCell.r][randomCell.c] = Math.random() < 0.9 ? 2 : 4;
|
||||
}
|
||||
}
|
||||
|
||||
move(direction) {
|
||||
// This function will handle the logic for moving tiles
|
||||
// in the specified direction and merging them
|
||||
// It will also update the score and add a new random tile if the move is successful
|
||||
// The actual implementation of this function is complex and would require
|
||||
// a significant amount of code to handle all the cases for moving and merging tiles
|
||||
// For the purposes of this example, we will not implement the full logic
|
||||
// Instead, we will just call addRandomTile to simulate a move
|
||||
this.addRandomTile();
|
||||
}
|
||||
|
||||
getBoard() {
|
||||
return this.board;
|
||||
}
|
||||
|
||||
getScore() {
|
||||
return this.score;
|
||||
}
|
||||
|
||||
getBestScore() {
|
||||
return this.bestScore;
|
||||
}
|
||||
|
||||
setBestScore(score) {
|
||||
this.bestScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
CODE_REVIEW_SMALLEST_CONTEXT = """
|
||||
## Code to be Reviewed: game.js
|
||||
```Code
|
||||
// game.js
|
||||
class Game {
|
||||
constructor() {
|
||||
this.board = this.createEmptyBoard();
|
||||
this.score = 0;
|
||||
this.bestScore = 0;
|
||||
}
|
||||
|
||||
createEmptyBoard() {
|
||||
const board = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
board[i] = [0, 0, 0, 0];
|
||||
}
|
||||
return board;
|
||||
}
|
||||
|
||||
startGame() {
|
||||
this.board = this.createEmptyBoard();
|
||||
this.score = 0;
|
||||
this.addRandomTile();
|
||||
this.addRandomTile();
|
||||
}
|
||||
|
||||
addRandomTile() {
|
||||
let emptyCells = [];
|
||||
for (let r = 0; r < 4; r++) {
|
||||
for (let c = 0; c < 4; c++) {
|
||||
if (this.board[r][c] === 0) {
|
||||
emptyCells.push({ r, c });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (emptyCells.length > 0) {
|
||||
let randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
|
||||
this.board[randomCell.r][randomCell.c] = Math.random() < 0.9 ? 2 : 4;
|
||||
}
|
||||
}
|
||||
|
||||
move(direction) {
|
||||
// This function will handle the logic for moving tiles
|
||||
// in the specified direction and merging them
|
||||
// It will also update the score and add a new random tile if the move is successful
|
||||
// The actual implementation of this function is complex and would require
|
||||
// a significant amount of code to handle all the cases for moving and merging tiles
|
||||
// For the purposes of this example, we will not implement the full logic
|
||||
// Instead, we will just call addRandomTile to simulate a move
|
||||
this.addRandomTile();
|
||||
}
|
||||
|
||||
getBoard() {
|
||||
return this.board;
|
||||
}
|
||||
|
||||
getScore() {
|
||||
return this.score;
|
||||
}
|
||||
|
||||
getBestScore() {
|
||||
return this.bestScore;
|
||||
}
|
||||
|
||||
setBestScore(score) {
|
||||
this.bestScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
CODE_REVIEW_SAMPLE = """
|
||||
## Code Review: game.js
|
||||
1. The code partially implements the requirements. The `Game` class is missing the full implementation of the `move` method, which is crucial for the game\'s functionality.
|
||||
2. The code logic is not completely correct. The `move` method is not implemented, which means the game cannot process player moves.
|
||||
3. The existing code follows the "Data structures and interfaces" in terms of class structure but lacks full method implementations.
|
||||
4. Not all functions are implemented. The `move` method is incomplete and does not handle the logic for moving and merging tiles.
|
||||
5. All necessary pre-dependencies seem to be imported since the code does not indicate the need for additional imports.
|
||||
6. The methods from other files (such as `Storage`) are not being used in the provided code snippet, but the class structure suggests that they will be used correctly.
|
||||
|
||||
## Actions
|
||||
1. Implement the `move` method to handle tile movements and merging. This is a complex task that requires careful consideration of the game\'s rules and logic. Here is a simplified version of how one might begin to implement the `move` method:
|
||||
```javascript
|
||||
move(direction) {
|
||||
// Simplified logic for moving tiles up
|
||||
if (direction === \'up\') {
|
||||
for (let col = 0; col < 4; col++) {
|
||||
let tiles = this.board.map(row => row[col]).filter(val => val !== 0);
|
||||
let merged = [];
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
if (tiles[i] === tiles[i + 1]) {
|
||||
tiles[i] *= 2;
|
||||
this.score += tiles[i];
|
||||
tiles[i + 1] = 0;
|
||||
merged.push(i);
|
||||
}
|
||||
}
|
||||
tiles = tiles.filter(val => val !== 0);
|
||||
while (tiles.length < 4) {
|
||||
tiles.push(0);
|
||||
}
|
||||
for (let row = 0; row < 4; row++) {
|
||||
this.board[row][col] = tiles[row];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Additional logic needed for \'down\', \'left\', \'right\'
|
||||
// ...
|
||||
this.addRandomTile();
|
||||
}
|
||||
```
|
||||
2. Integrate the `Storage` class methods to handle the best score. This means updating the `startGame` and `setBestScore` methods to use `Storage` for retrieving and setting the best score:
|
||||
```javascript
|
||||
startGame() {
|
||||
this.board = this.createEmptyBoard();
|
||||
this.score = 0;
|
||||
this.bestScore = new Storage().getBestScore(); // Retrieve the best score from storage
|
||||
this.addRandomTile();
|
||||
this.addRandomTile();
|
||||
}
|
||||
|
||||
setBestScore(score) {
|
||||
if (score > this.bestScore) {
|
||||
this.bestScore = score;
|
||||
new Storage().setBestScore(score); // Set the new best score in storage
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Code Review Result
|
||||
LBTM
|
||||
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
WRITE_CODE_NODE = ActionNode.from_children("WRITE_REVIEW_NODE", [REVIEW, LGTM, ACTIONS])
|
||||
WRITE_MOVE_NODE = ActionNode.from_children("WRITE_MOVE_NODE", [WRITE_DRAFT, WRITE_MOVE_FUNCTION])
|
||||
|
||||
|
||||
CR_FOR_MOVE_FUNCTION_BY_3 = """
|
||||
The move function implementation provided appears to be well-structured and follows a clear logic for moving and merging tiles in the specified direction. However, there are a few potential improvements that could be made to enhance the code:
|
||||
|
||||
1. Encapsulation: The logic for moving and merging tiles could be encapsulated into smaller, reusable functions to improve readability and maintainability.
|
||||
|
||||
2. Magic Numbers: There are some magic numbers (e.g., 4, 3) used in the loops that could be replaced with named constants for improved readability and easier maintenance.
|
||||
|
||||
3. Comments: Adding comments to explain the logic and purpose of each section of the code can improve understanding for future developers who may need to work on or maintain the code.
|
||||
|
||||
4. Error Handling: It's important to consider error handling for unexpected input or edge cases to ensure the function behaves as expected in all scenarios.
|
||||
|
||||
Overall, the code could benefit from refactoring to improve readability, maintainability, and extensibility. If you would like, I can provide a refactored version of the move function that addresses these considerations.
|
||||
"""
|
||||
|
||||
|
||||
class WriteCodeAN(Action):
|
||||
"""Write a code review for the context."""
|
||||
|
||||
async def run(self, context):
|
||||
self.llm.system_prompt = "You are an outstanding engineer and can implement any code"
|
||||
return await WRITE_MOVE_FUNCTION.fill(context=context, llm=self.llm, schema="json")
|
||||
# return await WRITE_CODE_NODE.fill(context=context, llm=self.llm, schema="markdown")
|
||||
|
||||
|
||||
async def main():
|
||||
await WriteCodeAN().run(CODE_REVIEW_SMALLEST_CONTEXT)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
@ -4,57 +4,116 @@
|
|||
@Time : 2023/5/11 17:45
|
||||
@Author : alexanderwu
|
||||
@File : write_code_review.py
|
||||
@Modified By: mashenquan, 2023/11/27. Following the think-act principle, solidify the task parameters when creating the
|
||||
WriteCode object, rather than passing them in when calling the run function.
|
||||
"""
|
||||
|
||||
from pydantic import Field
|
||||
from tenacity import retry, stop_after_attempt, wait_random_exponential
|
||||
|
||||
from metagpt.actions import WriteCode
|
||||
from metagpt.actions.action import Action
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.llm import LLM
|
||||
from metagpt.logs import logger
|
||||
from metagpt.schema import Message
|
||||
from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
from metagpt.schema import CodingContext
|
||||
from metagpt.utils.common import CodeParser
|
||||
from tenacity import retry, stop_after_attempt, wait_fixed
|
||||
|
||||
PROMPT_TEMPLATE = """
|
||||
NOTICE
|
||||
Role: You are a professional software engineer, and your main task is to review the code. You need to ensure that the code conforms to the PEP8 standards, is elegantly designed and modularized, easy to read and maintain, and is written in Python 3.9 (or in another programming language).
|
||||
# System
|
||||
Role: You are a professional software engineer, and your main task is to review and revise the code. You need to ensure that the code conforms to the google-style standards, is elegantly designed and modularized, easy to read and maintain.
|
||||
Language: Please use the same language as the user requirement, but the title and code should be still in English. For example, if the user speaks Chinese, the specific text of your answer should also be in Chinese.
|
||||
ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example".
|
||||
|
||||
## Code Review: Based on the following context and code, and following the check list, Provide key, clear, concise, and specific code modification suggestions, up to 5.
|
||||
```
|
||||
1. Check 0: Is the code implemented as per the requirements?
|
||||
2. Check 1: Are there any issues with the code logic?
|
||||
3. Check 2: Does the existing code follow the "Data structures and interface definitions"?
|
||||
4. Check 3: Is there a function in the code that is omitted or not fully implemented that needs to be implemented?
|
||||
5. Check 4: Does the code have unnecessary or lack dependencies?
|
||||
```
|
||||
|
||||
## Rewrite Code: {filename} Base on "Code Review" and the source code, rewrite code with triple quotes. Do your utmost to optimize THIS SINGLE FILE.
|
||||
-----
|
||||
# Context
|
||||
{context}
|
||||
|
||||
## Code: {filename}
|
||||
```
|
||||
## Code to be Reviewed: {filename}
|
||||
```Code
|
||||
{code}
|
||||
```
|
||||
-----
|
||||
"""
|
||||
|
||||
EXAMPLE_AND_INSTRUCTION = """
|
||||
|
||||
## Format example
|
||||
-----
|
||||
{format_example}
|
||||
-----
|
||||
|
||||
|
||||
# Instruction: Based on the actual code situation, follow one of the "Format example".
|
||||
|
||||
## Code Review: Ordered List. Based on the "Code to be Reviewed", provide key, clear, concise, and specific answer. If any answer is no, explain how to fix it step by step.
|
||||
1. Is the code implemented as per the requirements? If not, how to achieve it? Analyse it step by step.
|
||||
2. Is the code logic completely correct? If there are errors, please indicate how to correct them.
|
||||
3. Does the existing code follow the "Data structures and interfaces"?
|
||||
4. Are all functions implemented? If there is no implementation, please indicate how to achieve it step by step.
|
||||
5. Have all necessary pre-dependencies been imported? If not, indicate which ones need to be imported
|
||||
6. Are methods from other files being reused correctly?
|
||||
|
||||
## Actions: Ordered List. Things that should be done after CR, such as implementing class A and function B
|
||||
|
||||
## Code Review Result: str. If the code doesn't have bugs, we don't need to rewrite it, so answer LGTM and stop. ONLY ANSWER LGTM/LBTM.
|
||||
LGTM/LBTM
|
||||
|
||||
"""
|
||||
|
||||
FORMAT_EXAMPLE = """
|
||||
|
||||
## Code Review
|
||||
1. The code ...
|
||||
# Format example 1
|
||||
## Code Review: {filename}
|
||||
1. No, we should fix the logic of class A due to ...
|
||||
2. ...
|
||||
3. ...
|
||||
4. ...
|
||||
4. No, function B is not implemented, ...
|
||||
5. ...
|
||||
6. ...
|
||||
|
||||
## Rewrite Code: {filename}
|
||||
```python
|
||||
## Actions
|
||||
1. Fix the `handle_events` method to update the game state only if a move is successful.
|
||||
```python
|
||||
def handle_events(self):
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
return False
|
||||
if event.type == pygame.KEYDOWN:
|
||||
moved = False
|
||||
if event.key == pygame.K_UP:
|
||||
moved = self.game.move('UP')
|
||||
elif event.key == pygame.K_DOWN:
|
||||
moved = self.game.move('DOWN')
|
||||
elif event.key == pygame.K_LEFT:
|
||||
moved = self.game.move('LEFT')
|
||||
elif event.key == pygame.K_RIGHT:
|
||||
moved = self.game.move('RIGHT')
|
||||
if moved:
|
||||
# Update the game state only if a move was successful
|
||||
self.render()
|
||||
return True
|
||||
```
|
||||
2. Implement function B
|
||||
|
||||
## Code Review Result
|
||||
LBTM
|
||||
|
||||
# Format example 2
|
||||
## Code Review: {filename}
|
||||
1. Yes.
|
||||
2. Yes.
|
||||
3. Yes.
|
||||
4. Yes.
|
||||
5. Yes.
|
||||
6. Yes.
|
||||
|
||||
## Actions
|
||||
pass
|
||||
|
||||
## Code Review Result
|
||||
LGTM
|
||||
"""
|
||||
|
||||
REWRITE_CODE_TEMPLATE = """
|
||||
# Instruction: rewrite code based on the Code Review and Actions
|
||||
## Rewrite Code: CodeBlock. If it still has some bugs, rewrite {filename} with triple quotes. Do your utmost to optimize THIS SINGLE FILE. Return all completed codes and prohibit the return of unfinished codes.
|
||||
```Code
|
||||
## {filename}
|
||||
...
|
||||
```
|
||||
|
|
@ -62,21 +121,59 @@ FORMAT_EXAMPLE = """
|
|||
|
||||
|
||||
class WriteCodeReview(Action):
|
||||
def __init__(self, name="WriteCodeReview", context: list[Message] = None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
name: str = "WriteCodeReview"
|
||||
context: CodingContext = Field(default_factory=CodingContext)
|
||||
llm: BaseGPTAPI = Field(default_factory=LLM)
|
||||
|
||||
@retry(stop=stop_after_attempt(2), wait=wait_fixed(1))
|
||||
async def write_code(self, prompt):
|
||||
code_rsp = await self._aask(prompt)
|
||||
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
|
||||
async def write_code_review_and_rewrite(self, context_prompt, cr_prompt, filename):
|
||||
cr_rsp = await self._aask(context_prompt + cr_prompt)
|
||||
result = CodeParser.parse_block("Code Review Result", cr_rsp)
|
||||
if "LGTM" in result:
|
||||
return result, None
|
||||
|
||||
# if LBTM, rewrite code
|
||||
rewrite_prompt = f"{context_prompt}\n{cr_rsp}\n{REWRITE_CODE_TEMPLATE.format(filename=filename)}"
|
||||
code_rsp = await self._aask(rewrite_prompt)
|
||||
code = CodeParser.parse_code(block="", text=code_rsp)
|
||||
return code
|
||||
return result, code
|
||||
|
||||
async def run(self, context, code, filename):
|
||||
format_example = FORMAT_EXAMPLE.format(filename=filename)
|
||||
prompt = PROMPT_TEMPLATE.format(context=context, code=code, filename=filename, format_example=format_example)
|
||||
logger.info(f'Code review {filename}..')
|
||||
code = await self.write_code(prompt)
|
||||
async def run(self, *args, **kwargs) -> CodingContext:
|
||||
iterative_code = self.context.code_doc.content
|
||||
k = CONFIG.code_review_k_times or 1
|
||||
for i in range(k):
|
||||
format_example = FORMAT_EXAMPLE.format(filename=self.context.code_doc.filename)
|
||||
task_content = self.context.task_doc.content if self.context.task_doc else ""
|
||||
code_context = await WriteCode.get_codes(self.context.task_doc, exclude=self.context.filename)
|
||||
context = "\n".join(
|
||||
[
|
||||
"## System Design\n" + str(self.context.design_doc) + "\n",
|
||||
"## Tasks\n" + task_content + "\n",
|
||||
"## Code Files\n" + code_context + "\n",
|
||||
]
|
||||
)
|
||||
context_prompt = PROMPT_TEMPLATE.format(
|
||||
context=context,
|
||||
code=iterative_code,
|
||||
filename=self.context.code_doc.filename,
|
||||
)
|
||||
cr_prompt = EXAMPLE_AND_INSTRUCTION.format(
|
||||
format_example=format_example,
|
||||
)
|
||||
logger.info(
|
||||
f"Code review and rewrite {self.context.code_doc.filename}: {i + 1}/{k} | {len(iterative_code)=}, "
|
||||
f"{len(self.context.code_doc.content)=}"
|
||||
)
|
||||
result, rewrited_code = await self.write_code_review_and_rewrite(
|
||||
context_prompt, cr_prompt, self.context.code_doc.filename
|
||||
)
|
||||
if "LBTM" in result:
|
||||
iterative_code = rewrited_code
|
||||
elif "LGTM" in result:
|
||||
self.context.code_doc.content = iterative_code
|
||||
return self.context
|
||||
# code_rsp = await self._aask_v1(prompt, "code_rsp", OUTPUT_MAPPING)
|
||||
# self._save(context, filename, code)
|
||||
return code
|
||||
|
||||
# 如果rewrited_code是None(原code perfect),那么直接返回code
|
||||
self.context.code_doc.content = iterative_code
|
||||
return self.context
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Options:
|
|||
Default: 'google'
|
||||
|
||||
Example:
|
||||
python3 -m metagpt.actions.write_docstring startup.py --overwrite False --style=numpy
|
||||
python3 -m metagpt.actions.write_docstring ./metagpt/startup.py --overwrite False --style=numpy
|
||||
|
||||
This script uses the 'fire' library to create a command-line interface. It generates docstrings for the given Python code using
|
||||
the specified docstring style and adds them to the code.
|
||||
|
|
@ -28,7 +28,7 @@ from metagpt.actions.action import Action
|
|||
from metagpt.utils.common import OutputParser
|
||||
from metagpt.utils.pycst import merge_docstring
|
||||
|
||||
PYTHON_DOCSTRING_SYSTEM = '''### Requirements
|
||||
PYTHON_DOCSTRING_SYSTEM = """### Requirements
|
||||
1. Add docstrings to the given code following the {style} style.
|
||||
2. Replace the function body with an Ellipsis object(...) to reduce output.
|
||||
3. If the types are already annotated, there is no need to include them in the docstring.
|
||||
|
|
@ -48,7 +48,7 @@ class ExampleError(Exception):
|
|||
```python
|
||||
{example}
|
||||
```
|
||||
'''
|
||||
"""
|
||||
|
||||
# https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html
|
||||
|
||||
|
|
@ -162,7 +162,8 @@ class WriteDocstring(Action):
|
|||
self.desc = "Write docstring for code."
|
||||
|
||||
async def run(
|
||||
self, code: str,
|
||||
self,
|
||||
code: str,
|
||||
system_text: str = PYTHON_DOCSTRING_SYSTEM,
|
||||
style: Literal["google", "numpy", "sphinx"] = "google",
|
||||
) -> str:
|
||||
|
|
|
|||
|
|
@ -4,238 +4,199 @@
|
|||
@Time : 2023/5/11 17:45
|
||||
@Author : alexanderwu
|
||||
@File : write_prd.py
|
||||
@Modified By: mashenquan, 2023/11/27.
|
||||
1. According to Section 2.2.3.1 of RFC 135, replace file data in the message with the file name.
|
||||
2. According to the design in Section 2.2.3.5.2 of RFC 135, add incremental iteration functionality.
|
||||
3. Move the document storage operations related to WritePRD from the save operation of WriteDesign.
|
||||
@Modified By: mashenquan, 2023/12/5. Move the generation logic of the project name to WritePRD.
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from metagpt.actions import Action, ActionOutput
|
||||
from metagpt.actions.search_and_summarize import SearchAndSummarize
|
||||
from metagpt.actions.action_node import ActionNode
|
||||
from metagpt.actions.fix_bug import FixBug
|
||||
from metagpt.actions.write_prd_an import (
|
||||
WP_IS_RELATIVE_NODE,
|
||||
WP_ISSUE_TYPE_NODE,
|
||||
WRITE_PRD_NODE,
|
||||
)
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.const import (
|
||||
BUGFIX_FILENAME,
|
||||
COMPETITIVE_ANALYSIS_FILE_REPO,
|
||||
DOCS_FILE_REPO,
|
||||
PRD_PDF_FILE_REPO,
|
||||
PRDS_FILE_REPO,
|
||||
REQUIREMENT_FILENAME,
|
||||
)
|
||||
from metagpt.llm import LLM
|
||||
from metagpt.logs import logger
|
||||
from metagpt.utils.get_template import get_template
|
||||
from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
from metagpt.schema import BugFixContext, Document, Documents, Message
|
||||
from metagpt.utils.common import CodeParser
|
||||
from metagpt.utils.file_repository import FileRepository
|
||||
from metagpt.utils.mermaid import mermaid_to_file
|
||||
|
||||
templates = {
|
||||
"json": {
|
||||
"PROMPT_TEMPLATE": """
|
||||
# Context
|
||||
## Original Requirements
|
||||
CONTEXT_TEMPLATE = """
|
||||
### Project Name
|
||||
{project_name}
|
||||
|
||||
### Original Requirements
|
||||
{requirements}
|
||||
|
||||
## Search Information
|
||||
{search_information}
|
||||
### Search Information
|
||||
-
|
||||
"""
|
||||
|
||||
## mermaid quadrantChart code syntax example. DONT USE QUOTO IN CODE DUE TO INVALID SYNTAX. Replace the <Campain X> with REAL COMPETITOR NAME
|
||||
```mermaid
|
||||
quadrantChart
|
||||
title Reach and engagement of campaigns
|
||||
x-axis Low Reach --> High Reach
|
||||
y-axis Low Engagement --> High Engagement
|
||||
quadrant-1 We should expand
|
||||
quadrant-2 Need to promote
|
||||
quadrant-3 Re-evaluate
|
||||
quadrant-4 May be improved
|
||||
"Campaign: A": [0.3, 0.6]
|
||||
"Campaign B": [0.45, 0.23]
|
||||
"Campaign C": [0.57, 0.69]
|
||||
"Campaign D": [0.78, 0.34]
|
||||
"Campaign E": [0.40, 0.34]
|
||||
"Campaign F": [0.35, 0.78]
|
||||
"Our Target Product": [0.5, 0.6]
|
||||
```
|
||||
NEW_REQ_TEMPLATE = """
|
||||
### Legacy Content
|
||||
{old_prd}
|
||||
|
||||
## Format example
|
||||
{format_example}
|
||||
-----
|
||||
Role: You are a professional product manager; the goal is to design a concise, usable, efficient product
|
||||
Requirements: According to the context, fill in the following missing information, each section name is a key in json ,If the requirements are unclear, ensure minimum viability and avoid excessive design
|
||||
|
||||
## Original Requirements: Provide as Plain text, place the polished complete original requirements here
|
||||
|
||||
## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. If the requirement itself is simple, the goal should also be simple
|
||||
|
||||
## User Stories: Provided as Python list[str], up to 5 scenario-based user stories, If the requirement itself is simple, the user stories should also be less
|
||||
|
||||
## Competitive Analysis: Provided as Python list[str], up to 7 competitive product analyses, consider as similar competitors as possible
|
||||
|
||||
## Competitive Quadrant Chart: Use mermaid quadrantChart code syntax. up to 14 competitive products. Translation: Distribute these competitor scores evenly between 0 and 1, trying to conform to a normal distribution centered around 0.5 as much as possible.
|
||||
|
||||
## Requirement Analysis: Provide as Plain text. Be simple. LESS IS MORE. Make your requirements less dumb. Delete the parts unnessasery.
|
||||
|
||||
## Requirement Pool: Provided as Python list[list[str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards; no more than 5 requirements and consider to make its difficulty lower
|
||||
|
||||
## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description.
|
||||
## Anything UNCLEAR: Provide as Plain text. Make clear here.
|
||||
|
||||
output a properly formatted JSON, wrapped inside [CONTENT][/CONTENT] like format example,
|
||||
and only output the json inside this tag, nothing else
|
||||
""",
|
||||
"FORMAT_EXAMPLE": """
|
||||
[CONTENT]
|
||||
{
|
||||
"Original Requirements": "",
|
||||
"Search Information": "",
|
||||
"Requirements": "",
|
||||
"Product Goals": [],
|
||||
"User Stories": [],
|
||||
"Competitive Analysis": [],
|
||||
"Competitive Quadrant Chart": "quadrantChart
|
||||
title Reach and engagement of campaigns
|
||||
x-axis Low Reach --> High Reach
|
||||
y-axis Low Engagement --> High Engagement
|
||||
quadrant-1 We should expand
|
||||
quadrant-2 Need to promote
|
||||
quadrant-3 Re-evaluate
|
||||
quadrant-4 May be improved
|
||||
Campaign A: [0.3, 0.6]
|
||||
Campaign B: [0.45, 0.23]
|
||||
Campaign C: [0.57, 0.69]
|
||||
Campaign D: [0.78, 0.34]
|
||||
Campaign E: [0.40, 0.34]
|
||||
Campaign F: [0.35, 0.78]",
|
||||
"Requirement Analysis": "",
|
||||
"Requirement Pool": [["P0","P0 requirement"],["P1","P1 requirement"]],
|
||||
"UI Design draft": "",
|
||||
"Anything UNCLEAR": "",
|
||||
}
|
||||
[/CONTENT]
|
||||
""",
|
||||
},
|
||||
"markdown": {
|
||||
"PROMPT_TEMPLATE": """
|
||||
# Context
|
||||
## Original Requirements
|
||||
### New Requirements
|
||||
{requirements}
|
||||
|
||||
## Search Information
|
||||
{search_information}
|
||||
|
||||
## mermaid quadrantChart code syntax example. DONT USE QUOTO IN CODE DUE TO INVALID SYNTAX. Replace the <Campain X> with REAL COMPETITOR NAME
|
||||
```mermaid
|
||||
quadrantChart
|
||||
title Reach and engagement of campaigns
|
||||
x-axis Low Reach --> High Reach
|
||||
y-axis Low Engagement --> High Engagement
|
||||
quadrant-1 We should expand
|
||||
quadrant-2 Need to promote
|
||||
quadrant-3 Re-evaluate
|
||||
quadrant-4 May be improved
|
||||
"Campaign: A": [0.3, 0.6]
|
||||
"Campaign B": [0.45, 0.23]
|
||||
"Campaign C": [0.57, 0.69]
|
||||
"Campaign D": [0.78, 0.34]
|
||||
"Campaign E": [0.40, 0.34]
|
||||
"Campaign F": [0.35, 0.78]
|
||||
"Our Target Product": [0.5, 0.6]
|
||||
```
|
||||
|
||||
## Format example
|
||||
{format_example}
|
||||
-----
|
||||
Role: You are a professional product manager; the goal is to design a concise, usable, efficient product
|
||||
Requirements: According to the context, fill in the following missing information, note that each sections are returned in Python code triple quote form seperatedly. If the requirements are unclear, ensure minimum viability and avoid excessive design
|
||||
ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. AND '## <SECTION_NAME>' SHOULD WRITE BEFORE the code and triple quote. Output carefully referenced "Format example" in format.
|
||||
|
||||
## Original Requirements: Provide as Plain text, place the polished complete original requirements here
|
||||
|
||||
## Product Goals: Provided as Python list[str], up to 3 clear, orthogonal product goals. If the requirement itself is simple, the goal should also be simple
|
||||
|
||||
## User Stories: Provided as Python list[str], up to 5 scenario-based user stories, If the requirement itself is simple, the user stories should also be less
|
||||
|
||||
## Competitive Analysis: Provided as Python list[str], up to 7 competitive product analyses, consider as similar competitors as possible
|
||||
|
||||
## Competitive Quadrant Chart: Use mermaid quadrantChart code syntax. up to 14 competitive products. Translation: Distribute these competitor scores evenly between 0 and 1, trying to conform to a normal distribution centered around 0.5 as much as possible.
|
||||
|
||||
## Requirement Analysis: Provide as Plain text. Be simple. LESS IS MORE. Make your requirements less dumb. Delete the parts unnessasery.
|
||||
|
||||
## Requirement Pool: Provided as Python list[list[str], the parameters are requirement description, priority(P0/P1/P2), respectively, comply with PEP standards; no more than 5 requirements and consider to make its difficulty lower
|
||||
|
||||
## UI Design draft: Provide as Plain text. Be simple. Describe the elements and functions, also provide a simple style description and layout description.
|
||||
## Anything UNCLEAR: Provide as Plain text. Make clear here.
|
||||
""",
|
||||
"FORMAT_EXAMPLE": """
|
||||
---
|
||||
## Original Requirements
|
||||
The boss ...
|
||||
|
||||
## Product Goals
|
||||
```python
|
||||
[
|
||||
"Create a ...",
|
||||
]
|
||||
```
|
||||
|
||||
## User Stories
|
||||
```python
|
||||
[
|
||||
"As a user, ...",
|
||||
]
|
||||
```
|
||||
|
||||
## Competitive Analysis
|
||||
```python
|
||||
[
|
||||
"Python Snake Game: ...",
|
||||
]
|
||||
```
|
||||
|
||||
## Competitive Quadrant Chart
|
||||
```mermaid
|
||||
quadrantChart
|
||||
title Reach and engagement of campaigns
|
||||
...
|
||||
"Our Target Product": [0.6, 0.7]
|
||||
```
|
||||
|
||||
## Requirement Analysis
|
||||
The product should be a ...
|
||||
|
||||
## Requirement Pool
|
||||
```python
|
||||
[
|
||||
["End game ...", "P0"]
|
||||
]
|
||||
```
|
||||
|
||||
## UI Design draft
|
||||
Give a basic function description, and a draft
|
||||
|
||||
## Anything UNCLEAR
|
||||
There are no unclear points.
|
||||
---
|
||||
""",
|
||||
},
|
||||
}
|
||||
|
||||
OUTPUT_MAPPING = {
|
||||
"Original Requirements": (str, ...),
|
||||
"Product Goals": (List[str], ...),
|
||||
"User Stories": (List[str], ...),
|
||||
"Competitive Analysis": (List[str], ...),
|
||||
"Competitive Quadrant Chart": (str, ...),
|
||||
"Requirement Analysis": (str, ...),
|
||||
"Requirement Pool": (List[List[str]], ...),
|
||||
"UI Design draft": (str, ...),
|
||||
"Anything UNCLEAR": (str, ...),
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class WritePRD(Action):
|
||||
def __init__(self, name="", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
name: str = ""
|
||||
content: Optional[str] = None
|
||||
llm: BaseGPTAPI = Field(default_factory=LLM)
|
||||
|
||||
async def run(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 = ""
|
||||
info = f"### Search Results\n{sas.result}\n\n### Search Summary\n{rsp}"
|
||||
if sas.result:
|
||||
logger.info(sas.result)
|
||||
logger.info(rsp)
|
||||
async def run(self, with_messages, schema=CONFIG.prompt_schema, *args, **kwargs) -> ActionOutput | Message:
|
||||
# Determine which requirement documents need to be rewritten: Use LLM to assess whether new requirements are
|
||||
# related to the PRD. If they are related, rewrite the PRD.
|
||||
docs_file_repo = CONFIG.git_repo.new_file_repository(relative_path=DOCS_FILE_REPO)
|
||||
requirement_doc = await docs_file_repo.get(filename=REQUIREMENT_FILENAME)
|
||||
if requirement_doc and await self._is_bugfix(requirement_doc.content):
|
||||
await docs_file_repo.save(filename=BUGFIX_FILENAME, content=requirement_doc.content)
|
||||
await docs_file_repo.save(filename=REQUIREMENT_FILENAME, content="")
|
||||
bug_fix = BugFixContext(filename=BUGFIX_FILENAME)
|
||||
return Message(
|
||||
content=bug_fix.json(),
|
||||
instruct_content=bug_fix,
|
||||
role="",
|
||||
cause_by=FixBug,
|
||||
sent_from=self,
|
||||
send_to="Alex", # the name of Engineer
|
||||
)
|
||||
else:
|
||||
await docs_file_repo.delete(filename=BUGFIX_FILENAME)
|
||||
|
||||
prompt_template, format_example = get_template(templates, format)
|
||||
prompt = prompt_template.format(
|
||||
requirements=requirements, search_information=info, format_example=format_example
|
||||
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:
|
||||
prd_doc = await self._update_prd(
|
||||
requirement_doc=requirement_doc, prd_doc=prd_doc, prds_file_repo=prds_file_repo, *args, **kwargs
|
||||
)
|
||||
if not prd_doc:
|
||||
continue
|
||||
change_files.docs[prd_doc.filename] = prd_doc
|
||||
logger.info(f"rewrite prd: {prd_doc.filename}")
|
||||
# If there is no existing PRD, generate one using 'docs/requirement.txt'.
|
||||
if not change_files.docs:
|
||||
prd_doc = await self._update_prd(
|
||||
requirement_doc=requirement_doc, prd_doc=None, prds_file_repo=prds_file_repo, *args, **kwargs
|
||||
)
|
||||
if prd_doc:
|
||||
change_files.docs[prd_doc.filename] = prd_doc
|
||||
logger.debug(f"new prd: {prd_doc.filename}")
|
||||
# Once all files under 'docs/prds/' have been compared with the newly added requirements, trigger the
|
||||
# 'publish' message to transition the workflow to the next stage. This design allows room for global
|
||||
# optimization in subsequent steps.
|
||||
return ActionOutput(content=change_files.json(), instruct_content=change_files)
|
||||
|
||||
async def _run_new_requirement(self, requirements, schema=CONFIG.prompt_schema) -> ActionOutput:
|
||||
# sas = SearchAndSummarize()
|
||||
# # rsp = await sas.run(context=requirements, system_text=SEARCH_AND_SUMMARIZE_SYSTEM_EN_US)
|
||||
# rsp = ""
|
||||
# info = f"### Search Results\n{sas.result}\n\n### Search Summary\n{rsp}"
|
||||
# if sas.result:
|
||||
# logger.info(sas.result)
|
||||
# logger.info(rsp)
|
||||
project_name = CONFIG.project_name if CONFIG.project_name else ""
|
||||
context = CONTEXT_TEMPLATE.format(requirements=requirements, project_name=project_name)
|
||||
node = await WRITE_PRD_NODE.fill(context=context, llm=self.llm, schema=schema)
|
||||
await self._rename_workspace(node)
|
||||
return node
|
||||
|
||||
async def _is_relative(self, new_requirement_doc, old_prd_doc) -> bool:
|
||||
context = NEW_REQ_TEMPLATE.format(old_prd=old_prd_doc.content, requirements=new_requirement_doc.content)
|
||||
node = await WP_IS_RELATIVE_NODE.fill(context, self.llm)
|
||||
return node.get("is_relative") == "YES"
|
||||
|
||||
async def _merge(self, new_requirement_doc, prd_doc, schema=CONFIG.prompt_schema) -> Document:
|
||||
if not CONFIG.project_name:
|
||||
CONFIG.project_name = Path(CONFIG.project_path).name
|
||||
prompt = NEW_REQ_TEMPLATE.format(requirements=new_requirement_doc.content, old_prd=prd_doc.content)
|
||||
node = await WRITE_PRD_NODE.fill(context=prompt, llm=self.llm, schema=schema)
|
||||
prd_doc.content = node.instruct_content.json(ensure_ascii=False)
|
||||
await self._rename_workspace(node)
|
||||
return prd_doc
|
||||
|
||||
async def _update_prd(self, requirement_doc, prd_doc, prds_file_repo, *args, **kwargs) -> Document | None:
|
||||
if not prd_doc:
|
||||
prd = await self._run_new_requirement(
|
||||
requirements=[requirement_doc.content if requirement_doc else ""], *args, **kwargs
|
||||
)
|
||||
new_prd_doc = Document(
|
||||
root_path=PRDS_FILE_REPO,
|
||||
filename=FileRepository.new_filename() + ".json",
|
||||
content=prd.instruct_content.json(ensure_ascii=False),
|
||||
)
|
||||
elif await self._is_relative(requirement_doc, prd_doc):
|
||||
new_prd_doc = await self._merge(requirement_doc, prd_doc)
|
||||
else:
|
||||
return None
|
||||
await prds_file_repo.save(filename=new_prd_doc.filename, content=new_prd_doc.content)
|
||||
await self._save_competitive_analysis(new_prd_doc)
|
||||
await self._save_pdf(new_prd_doc)
|
||||
return new_prd_doc
|
||||
|
||||
@staticmethod
|
||||
async def _save_competitive_analysis(prd_doc):
|
||||
m = json.loads(prd_doc.content)
|
||||
quadrant_chart = m.get("Competitive Quadrant Chart")
|
||||
if not quadrant_chart:
|
||||
return
|
||||
pathname = (
|
||||
CONFIG.git_repo.workdir / Path(COMPETITIVE_ANALYSIS_FILE_REPO) / Path(prd_doc.filename).with_suffix("")
|
||||
)
|
||||
logger.debug(prompt)
|
||||
# prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING)
|
||||
prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING, format=format)
|
||||
return prd
|
||||
if not pathname.parent.exists():
|
||||
pathname.parent.mkdir(parents=True, exist_ok=True)
|
||||
await mermaid_to_file(quadrant_chart, pathname)
|
||||
|
||||
@staticmethod
|
||||
async def _save_pdf(prd_doc):
|
||||
await FileRepository.save_as(doc=prd_doc, with_suffix=".md", relative_path=PRD_PDF_FILE_REPO)
|
||||
|
||||
@staticmethod
|
||||
async def _rename_workspace(prd):
|
||||
if CONFIG.project_path: # Updating on the old version has already been specified if it's valid. According to
|
||||
# Section 2.2.3.10 of RFC 135
|
||||
if not CONFIG.project_name:
|
||||
CONFIG.project_name = Path(CONFIG.project_path).name
|
||||
return
|
||||
|
||||
if not CONFIG.project_name:
|
||||
if isinstance(prd, (ActionOutput, ActionNode)):
|
||||
ws_name = prd.instruct_content.dict()["Project Name"]
|
||||
else:
|
||||
ws_name = CodeParser.parse_str(block="Project Name", text=prd)
|
||||
CONFIG.project_name = ws_name
|
||||
CONFIG.git_repo.rename_root(CONFIG.project_name)
|
||||
|
||||
async def _is_bugfix(self, context) -> bool:
|
||||
src_workspace_path = CONFIG.git_repo.workdir / CONFIG.git_repo.workdir.name
|
||||
code_files = CONFIG.git_repo.get_files(relative_path=src_workspace_path)
|
||||
if not code_files:
|
||||
return False
|
||||
node = await WP_ISSUE_TYPE_NODE.fill(context, self.llm)
|
||||
return node.get("issue_type") == "BUG"
|
||||
|
|
|
|||
166
metagpt/actions/write_prd_an.py
Normal file
166
metagpt/actions/write_prd_an.py
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/12/14 11:40
|
||||
@Author : alexanderwu
|
||||
@File : write_prd_an.py
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
from metagpt.actions.action_node import ActionNode
|
||||
from metagpt.logs import logger
|
||||
|
||||
LANGUAGE = ActionNode(
|
||||
key="Language",
|
||||
expected_type=str,
|
||||
instruction="Provide the language used in the project, typically matching the user's requirement language.",
|
||||
example="en_us",
|
||||
)
|
||||
|
||||
PROGRAMMING_LANGUAGE = ActionNode(
|
||||
key="Programming Language",
|
||||
expected_type=str,
|
||||
instruction="Python/JavaScript or other mainstream programming language.",
|
||||
example="Python",
|
||||
)
|
||||
|
||||
ORIGINAL_REQUIREMENTS = ActionNode(
|
||||
key="Original Requirements",
|
||||
expected_type=str,
|
||||
instruction="Place the original user's requirements here.",
|
||||
example="Create a 2048 game",
|
||||
)
|
||||
|
||||
PROJECT_NAME = ActionNode(
|
||||
key="Project Name",
|
||||
expected_type=str,
|
||||
instruction="Name the project using snake case style, like 'game_2048' or 'simple_crm'.",
|
||||
example="game_2048",
|
||||
)
|
||||
|
||||
PRODUCT_GOALS = ActionNode(
|
||||
key="Product Goals",
|
||||
expected_type=List[str],
|
||||
instruction="Provide up to three clear, orthogonal product goals.",
|
||||
example=["Create an engaging user experience", "Improve accessibility, be responsive", "More beautiful UI"],
|
||||
)
|
||||
|
||||
USER_STORIES = ActionNode(
|
||||
key="User Stories",
|
||||
expected_type=List[str],
|
||||
instruction="Provide up to 3 to 5 scenario-based user stories.",
|
||||
example=[
|
||||
"As a player, I want to be able to choose difficulty levels",
|
||||
"As a player, I want to see my score after each game",
|
||||
"As a player, I want to get restart button when I lose",
|
||||
"As a player, I want to see beautiful UI that make me feel good",
|
||||
"As a player, I want to play game via mobile phone",
|
||||
],
|
||||
)
|
||||
|
||||
COMPETITIVE_ANALYSIS = ActionNode(
|
||||
key="Competitive Analysis",
|
||||
expected_type=List[str],
|
||||
instruction="Provide 5 to 7 competitive products.",
|
||||
example=[
|
||||
"2048 Game A: Simple interface, lacks responsive features",
|
||||
"play2048.co: Beautiful and responsive UI with my best score shown",
|
||||
"2048game.com: Responsive UI with my best score shown, but many ads",
|
||||
],
|
||||
)
|
||||
|
||||
COMPETITIVE_QUADRANT_CHART = ActionNode(
|
||||
key="Competitive Quadrant Chart",
|
||||
expected_type=str,
|
||||
instruction="Use mermaid quadrantChart syntax. Distribute scores evenly between 0 and 1",
|
||||
example="""quadrantChart
|
||||
title "Reach and engagement of campaigns"
|
||||
x-axis "Low Reach" --> "High Reach"
|
||||
y-axis "Low Engagement" --> "High Engagement"
|
||||
quadrant-1 "We should expand"
|
||||
quadrant-2 "Need to promote"
|
||||
quadrant-3 "Re-evaluate"
|
||||
quadrant-4 "May be improved"
|
||||
"Campaign A": [0.3, 0.6]
|
||||
"Campaign B": [0.45, 0.23]
|
||||
"Campaign C": [0.57, 0.69]
|
||||
"Campaign D": [0.78, 0.34]
|
||||
"Campaign E": [0.40, 0.34]
|
||||
"Campaign F": [0.35, 0.78]
|
||||
"Our Target Product": [0.5, 0.6]""",
|
||||
)
|
||||
|
||||
REQUIREMENT_ANALYSIS = ActionNode(
|
||||
key="Requirement Analysis",
|
||||
expected_type=str,
|
||||
instruction="Provide a detailed analysis of the requirements.",
|
||||
example="",
|
||||
)
|
||||
|
||||
REQUIREMENT_POOL = ActionNode(
|
||||
key="Requirement Pool",
|
||||
expected_type=List[List[str]],
|
||||
instruction="List down the top-5 requirements with their priority (P0, P1, P2).",
|
||||
example=[["P0", "The main code ..."], ["P0", "The game algorithm ..."]],
|
||||
)
|
||||
|
||||
UI_DESIGN_DRAFT = ActionNode(
|
||||
key="UI Design draft",
|
||||
expected_type=str,
|
||||
instruction="Provide a simple description of UI elements, functions, style, and layout.",
|
||||
example="Basic function description with a simple style and layout.",
|
||||
)
|
||||
|
||||
ANYTHING_UNCLEAR = ActionNode(
|
||||
key="Anything UNCLEAR",
|
||||
expected_type=str,
|
||||
instruction="Mention any aspects of the project that are unclear and try to clarify them.",
|
||||
example="",
|
||||
)
|
||||
|
||||
ISSUE_TYPE = ActionNode(
|
||||
key="issue_type",
|
||||
expected_type=str,
|
||||
instruction="Answer BUG/REQUIREMENT. If it is a bugfix, answer BUG, otherwise answer Requirement",
|
||||
example="BUG",
|
||||
)
|
||||
|
||||
IS_RELATIVE = ActionNode(
|
||||
key="is_relative",
|
||||
expected_type=str,
|
||||
instruction="Answer YES/NO. If the requirement is related to the old PRD, answer YES, otherwise NO",
|
||||
example="YES",
|
||||
)
|
||||
|
||||
REASON = ActionNode(
|
||||
key="reason", expected_type=str, instruction="Explain the reasoning process from question to answer", example="..."
|
||||
)
|
||||
|
||||
|
||||
NODES = [
|
||||
LANGUAGE,
|
||||
PROGRAMMING_LANGUAGE,
|
||||
ORIGINAL_REQUIREMENTS,
|
||||
PROJECT_NAME,
|
||||
PRODUCT_GOALS,
|
||||
USER_STORIES,
|
||||
COMPETITIVE_ANALYSIS,
|
||||
COMPETITIVE_QUADRANT_CHART,
|
||||
REQUIREMENT_ANALYSIS,
|
||||
REQUIREMENT_POOL,
|
||||
UI_DESIGN_DRAFT,
|
||||
ANYTHING_UNCLEAR,
|
||||
]
|
||||
|
||||
WRITE_PRD_NODE = ActionNode.from_children("WritePRD", NODES)
|
||||
WP_ISSUE_TYPE_NODE = ActionNode.from_children("WP_ISSUE_TYPE", [ISSUE_TYPE, REASON])
|
||||
WP_IS_RELATIVE_NODE = ActionNode.from_children("WP_IS_RELATIVE", [IS_RELATIVE, REASON])
|
||||
|
||||
|
||||
def main():
|
||||
prompt = WRITE_PRD_NODE.compile(context="")
|
||||
logger.info(prompt)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -5,24 +5,31 @@
|
|||
@Author : alexanderwu
|
||||
@File : write_prd_review.py
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from metagpt.actions.action import Action
|
||||
from metagpt.llm import LLM
|
||||
from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
|
||||
|
||||
class WritePRDReview(Action):
|
||||
def __init__(self, name, context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
self.prd = None
|
||||
self.desc = "Based on the PRD, conduct a PRD Review, providing clear and detailed feedback"
|
||||
self.prd_review_prompt_template = """
|
||||
Given the following Product Requirement Document (PRD):
|
||||
{prd}
|
||||
name: str = ""
|
||||
context: Optional[str] = None
|
||||
llm: BaseGPTAPI = Field(default_factory=LLM)
|
||||
prd: Optional[str] = None
|
||||
desc: str = "Based on the PRD, conduct a PRD Review, providing clear and detailed feedback"
|
||||
prd_review_prompt_template: str = """
|
||||
Given the following Product Requirement Document (PRD):
|
||||
{prd}
|
||||
|
||||
As a project manager, please review it and provide your feedback and suggestions.
|
||||
"""
|
||||
As a project manager, please review it and provide your feedback and suggestions.
|
||||
"""
|
||||
|
||||
async def run(self, prd):
|
||||
self.prd = prd
|
||||
prompt = self.prd_review_prompt_template.format(prd=self.prd)
|
||||
review = await self._aask(prompt)
|
||||
return review
|
||||
|
||||
37
metagpt/actions/write_review.py
Normal file
37
metagpt/actions/write_review.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Author : alexanderwu
|
||||
@File : write_review.py
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
from metagpt.actions import Action
|
||||
from metagpt.actions.action_node import ActionNode
|
||||
|
||||
REVIEW = ActionNode(
|
||||
key="Review",
|
||||
expected_type=List[str],
|
||||
instruction="Act as an experienced Reviewer and review the given output. Ask a series of critical questions, "
|
||||
"concisely and clearly, to help the writer improve their work.",
|
||||
example=[
|
||||
"This is a good PRD, but I think it can be improved by adding more details.",
|
||||
],
|
||||
)
|
||||
|
||||
LGTM = ActionNode(
|
||||
key="LGTM",
|
||||
expected_type=str,
|
||||
instruction="LGTM/LBTM. If the output is good enough, give a LGTM (Looks Good To Me) to the writer, "
|
||||
"else LBTM (Looks Bad To Me).",
|
||||
example="LGTM",
|
||||
)
|
||||
|
||||
WRITE_REVIEW_NODE = ActionNode.from_children("WRITE_REVIEW_NODE", [REVIEW, LGTM])
|
||||
|
||||
|
||||
class WriteReview(Action):
|
||||
"""Write a review for the given context."""
|
||||
|
||||
async def run(self, context):
|
||||
return await WRITE_REVIEW_NODE.fill(context=context, llm=self.llm, schema="json")
|
||||
|
|
@ -3,10 +3,22 @@
|
|||
"""
|
||||
@Time : 2023/5/11 22:12
|
||||
@Author : alexanderwu
|
||||
@File : environment.py
|
||||
@File : write_test.py
|
||||
@Modified By: mashenquan, 2023-11-27. Following the think-act principle, solidify the task parameters when creating the
|
||||
WriteTest object, rather than passing them in when calling the run function.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from metagpt.actions.action import Action
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.const import TEST_CODES_FILE_REPO
|
||||
from metagpt.llm import LLM
|
||||
from metagpt.logs import logger
|
||||
from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
from metagpt.schema import Document, TestingContext
|
||||
from metagpt.utils.common import CodeParser
|
||||
|
||||
PROMPT_TEMPLATE = """
|
||||
|
|
@ -15,7 +27,7 @@ NOTICE
|
|||
2. Requirement: Based on the context, develop a comprehensive test suite that adequately covers all relevant aspects of the code file under review. Your test suite will be part of the overall project QA, so please develop complete, robust, and reusable test cases.
|
||||
3. Attention1: Use '##' to split sections, not '#', and '## <SECTION_NAME>' SHOULD WRITE BEFORE the test case or script.
|
||||
4. Attention2: If there are any settings in your tests, ALWAYS SET A DEFAULT VALUE, ALWAYS USE STRONG TYPE AND EXPLICIT VARIABLE.
|
||||
5. Attention3: YOU MUST FOLLOW "Data structures and interface definitions". DO NOT CHANGE ANY DESIGN. Make sure your tests respect the existing design and ensure its validity.
|
||||
5. Attention3: YOU MUST FOLLOW "Data structures and interfaces". DO NOT CHANGE ANY DESIGN. Make sure your tests respect the existing design and ensure its validity.
|
||||
6. Think before writing: What should be tested and validated in this document? What edge cases could exist? What might fail?
|
||||
7. CAREFULLY CHECK THAT YOU DON'T MISS ANY NECESSARY TEST CASES/SCRIPTS IN THIS FILE.
|
||||
Attention: Use '##' to split sections, not '#', and '## <SECTION_NAME>' SHOULD WRITE BEFORE the test case or script and triple quotes.
|
||||
|
|
@ -26,13 +38,14 @@ Attention: Use '##' to split sections, not '#', and '## <SECTION_NAME>' SHOULD W
|
|||
```
|
||||
Note that the code to test is at {source_file_path}, we will put your test code at {workspace}/tests/{test_file_name}, and run your test code from {workspace},
|
||||
you should correctly import the necessary classes based on these file locations!
|
||||
## {test_file_name}: Write test code with triple quoto. Do your best to implement THIS ONLY ONE FILE.
|
||||
## {test_file_name}: Write test code with triple quote. Do your best to implement THIS ONLY ONE FILE.
|
||||
"""
|
||||
|
||||
|
||||
class WriteTest(Action):
|
||||
def __init__(self, name="WriteTest", context=None, llm=None):
|
||||
super().__init__(name, context, llm)
|
||||
name: str = "WriteTest"
|
||||
context: Optional[str] = None
|
||||
llm: BaseGPTAPI = Field(default_factory=LLM)
|
||||
|
||||
async def write_code(self, prompt):
|
||||
code_rsp = await self._aask(prompt)
|
||||
|
|
@ -47,12 +60,16 @@ class WriteTest(Action):
|
|||
code = code_rsp
|
||||
return code
|
||||
|
||||
async def run(self, code_to_test, test_file_name, source_file_path, workspace):
|
||||
async def run(self, *args, **kwargs) -> TestingContext:
|
||||
if not self.context.test_doc:
|
||||
self.context.test_doc = Document(
|
||||
filename="test_" + self.context.code_doc.filename, root_path=TEST_CODES_FILE_REPO
|
||||
)
|
||||
prompt = PROMPT_TEMPLATE.format(
|
||||
code_to_test=code_to_test,
|
||||
test_file_name=test_file_name,
|
||||
source_file_path=source_file_path,
|
||||
workspace=workspace,
|
||||
code_to_test=self.context.code_doc.content,
|
||||
test_file_name=self.context.test_doc.filename,
|
||||
source_file_path=self.context.code_doc.root_relative_path,
|
||||
workspace=CONFIG.git_repo.workdir,
|
||||
)
|
||||
code = await self.write_code(prompt)
|
||||
return code
|
||||
self.context.test_doc.content = await self.write_code(prompt)
|
||||
return self.context
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
from typing import Dict
|
||||
|
||||
from metagpt.actions import Action
|
||||
from metagpt.prompts.tutorial_assistant import DIRECTORY_PROMPT, CONTENT_PROMPT
|
||||
from metagpt.prompts.tutorial_assistant import CONTENT_PROMPT, DIRECTORY_PROMPT
|
||||
from metagpt.utils.common import OutputParser
|
||||
|
||||
|
||||
|
|
@ -65,4 +65,3 @@ class WriteContent(Action):
|
|||
"""
|
||||
prompt = CONTENT_PROMPT.format(topic=topic, language=self.language, directory=self.directory)
|
||||
return await self._aask(prompt=prompt)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Provide configuration, singleton
|
||||
@Modified By: mashenquan, 2023/11/27.
|
||||
1. According to Section 2.2.3.11 of RFC 135, add git repository support.
|
||||
2. Add the parameter `src_workspace` for the old version project path.
|
||||
"""
|
||||
import os
|
||||
from copy import deepcopy
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
from metagpt.const import PROJECT_ROOT
|
||||
from metagpt.const import DEFAULT_WORKSPACE_ROOT, METAGPT_ROOT, OPTIONS
|
||||
from metagpt.logs import logger
|
||||
from metagpt.tools import SearchEngineType, WebBrowserEngineType
|
||||
from metagpt.utils.singleton import Singleton
|
||||
|
|
@ -25,6 +32,15 @@ class NotConfiguredException(Exception):
|
|||
super().__init__(self.message)
|
||||
|
||||
|
||||
class LLMProviderEnum(Enum):
|
||||
OPENAI = "openai"
|
||||
ANTHROPIC = "anthropic"
|
||||
SPARK = "spark"
|
||||
ZHIPUAI = "zhipuai"
|
||||
FIREWORKS = "fireworks"
|
||||
OPEN_LLM = "open_llm"
|
||||
|
||||
|
||||
class Config(metaclass=Singleton):
|
||||
"""
|
||||
Regular usage method:
|
||||
|
|
@ -34,29 +50,60 @@ class Config(metaclass=Singleton):
|
|||
"""
|
||||
|
||||
_instance = None
|
||||
key_yaml_file = PROJECT_ROOT / "config/key.yaml"
|
||||
default_yaml_file = PROJECT_ROOT / "config/config.yaml"
|
||||
home_yaml_file = Path.home() / ".metagpt/config.yaml"
|
||||
key_yaml_file = METAGPT_ROOT / "config/key.yaml"
|
||||
default_yaml_file = METAGPT_ROOT / "config/config.yaml"
|
||||
|
||||
def __init__(self, yaml_file=default_yaml_file):
|
||||
self._configs = {}
|
||||
self._init_with_config_files_and_env(self._configs, yaml_file)
|
||||
logger.info("Config loading done.")
|
||||
global_options = OPTIONS.get()
|
||||
# cli paras
|
||||
self.project_path = ""
|
||||
self.project_name = ""
|
||||
self.inc = False
|
||||
self.reqa_file = ""
|
||||
self.max_auto_summarize_code = 0
|
||||
|
||||
self._init_with_config_files_and_env(yaml_file)
|
||||
self._update()
|
||||
global_options.update(OPTIONS.get())
|
||||
logger.debug("Config loading done.")
|
||||
|
||||
def get_default_llm_provider_enum(self) -> LLMProviderEnum:
|
||||
for k, v in [
|
||||
(self.openai_api_key, LLMProviderEnum.OPENAI),
|
||||
(self.anthropic_api_key, LLMProviderEnum.ANTHROPIC),
|
||||
(self.zhipuai_api_key, LLMProviderEnum.ZHIPUAI),
|
||||
(self.fireworks_api_key, LLMProviderEnum.FIREWORKS),
|
||||
(self.open_llm_api_base, LLMProviderEnum.OPEN_LLM), # reuse logic. but not a key
|
||||
]:
|
||||
if self._is_valid_llm_key(k):
|
||||
if self.openai_api_model:
|
||||
logger.info(f"OpenAI API Model: {self.openai_api_model}")
|
||||
return v
|
||||
raise NotConfiguredException("You should config a LLM configuration first")
|
||||
|
||||
@staticmethod
|
||||
def _is_valid_llm_key(k: str) -> bool:
|
||||
return k and k != "YOUR_API_KEY"
|
||||
|
||||
def _update(self):
|
||||
# logger.info("Config loading done.")
|
||||
self.global_proxy = self._get("GLOBAL_PROXY")
|
||||
|
||||
self.openai_api_key = self._get("OPENAI_API_KEY")
|
||||
self.anthropic_api_key = self._get("Anthropic_API_KEY")
|
||||
self.anthropic_api_key = self._get("ANTHROPIC_API_KEY")
|
||||
self.zhipuai_api_key = self._get("ZHIPUAI_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)
|
||||
and (not self.zhipuai_api_key or "YOUR_API_KEY" == self.zhipuai_api_key)
|
||||
):
|
||||
raise NotConfiguredException("Set OPENAI_API_KEY or Anthropic_API_KEY or ZHIPUAI_API_KEY first")
|
||||
self.open_llm_api_base = self._get("OPEN_LLM_API_BASE")
|
||||
self.open_llm_api_model = self._get("OPEN_LLM_API_MODEL")
|
||||
self.fireworks_api_key = self._get("FIREWORKS_API_KEY")
|
||||
_ = self.get_default_llm_provider_enum()
|
||||
|
||||
self.openai_base_url = self._get("OPENAI_BASE_URL")
|
||||
self.openai_proxy = self._get("OPENAI_PROXY") or self.global_proxy
|
||||
self.openai_api_type = self._get("OPENAI_API_TYPE")
|
||||
self.openai_api_version = self._get("OPENAI_API_VERSION")
|
||||
self.openai_api_rpm = self._get("RPM", 3)
|
||||
self.openai_api_model = self._get("OPENAI_API_MODEL", "gpt-4")
|
||||
self.openai_api_model = self._get("OPENAI_API_MODEL", "gpt-4-1106-preview")
|
||||
self.max_tokens_rsp = self._get("MAX_TOKENS", 2048)
|
||||
self.deployment_name = self._get("DEPLOYMENT_NAME", "gpt-4")
|
||||
|
||||
|
|
@ -66,7 +113,10 @@ class Config(metaclass=Singleton):
|
|||
self.domain = self._get("DOMAIN")
|
||||
self.spark_url = self._get("SPARK_URL")
|
||||
|
||||
self.claude_api_key = self._get("Anthropic_API_KEY")
|
||||
self.fireworks_api_base = self._get("FIREWORKS_API_BASE")
|
||||
self.fireworks_api_model = self._get("FIREWORKS_API_MODEL")
|
||||
|
||||
self.claude_api_key = self._get("ANTHROPIC_API_KEY")
|
||||
self.serpapi_api_key = self._get("SERPAPI_API_KEY")
|
||||
self.serper_api_key = self._get("SERPER_API_KEY")
|
||||
self.google_api_key = self._get("GOOGLE_API_KEY")
|
||||
|
|
@ -81,6 +131,7 @@ class Config(metaclass=Singleton):
|
|||
logger.warning("LONG_TERM_MEMORY is True")
|
||||
self.max_budget = self._get("MAX_BUDGET", 10.0)
|
||||
self.total_cost = 0.0
|
||||
self.code_review_k_times = 2
|
||||
|
||||
self.puppeteer_config = self._get("PUPPETEER_CONFIG", "")
|
||||
self.mmdc = self._get("MMDC", "mmdc")
|
||||
|
|
@ -90,13 +141,33 @@ class Config(metaclass=Singleton):
|
|||
self.mermaid_engine = self._get("MERMAID_ENGINE", "nodejs")
|
||||
self.pyppeteer_executable_path = self._get("PYPPETEER_EXECUTABLE_PATH", "")
|
||||
|
||||
self.prompt_format = self._get("PROMPT_FORMAT", "markdown")
|
||||
self.repair_llm_output = self._get("REPAIR_LLM_OUTPUT", False)
|
||||
self.prompt_schema = self._get("PROMPT_FORMAT", "json")
|
||||
self.workspace_path = Path(self._get("WORKSPACE_PATH", DEFAULT_WORKSPACE_ROOT))
|
||||
self._ensure_workspace_exists()
|
||||
|
||||
def _init_with_config_files_and_env(self, configs: dict, yaml_file):
|
||||
def update_via_cli(self, project_path, project_name, inc, reqa_file, max_auto_summarize_code):
|
||||
"""update config via cli"""
|
||||
|
||||
# Use in the PrepareDocuments action according to Section 2.2.3.5.1 of RFC 135.
|
||||
if project_path:
|
||||
inc = True
|
||||
project_name = project_name or Path(project_path).name
|
||||
self.project_path = project_path
|
||||
self.project_name = project_name
|
||||
self.inc = inc
|
||||
self.reqa_file = reqa_file
|
||||
self.max_auto_summarize_code = max_auto_summarize_code
|
||||
|
||||
def _ensure_workspace_exists(self):
|
||||
self.workspace_path.mkdir(parents=True, exist_ok=True)
|
||||
logger.debug(f"WORKSPACE_PATH set to {self.workspace_path}")
|
||||
|
||||
def _init_with_config_files_and_env(self, yaml_file):
|
||||
"""Load from config/key.yaml, config/config.yaml, and env in decreasing order of priority"""
|
||||
configs.update(os.environ)
|
||||
configs = dict(os.environ)
|
||||
|
||||
for _yaml_file in [yaml_file, self.key_yaml_file]:
|
||||
for _yaml_file in [yaml_file, self.key_yaml_file, self.home_yaml_file]:
|
||||
if not _yaml_file.exists():
|
||||
continue
|
||||
|
||||
|
|
@ -105,11 +176,13 @@ class Config(metaclass=Singleton):
|
|||
yaml_data = yaml.safe_load(file)
|
||||
if not yaml_data:
|
||||
continue
|
||||
os.environ.update({k: v for k, v in yaml_data.items() if isinstance(v, str)})
|
||||
configs.update(yaml_data)
|
||||
OPTIONS.set(configs)
|
||||
|
||||
def _get(self, *args, **kwargs):
|
||||
return self._configs.get(*args, **kwargs)
|
||||
@staticmethod
|
||||
def _get(*args, **kwargs):
|
||||
i = OPTIONS.get()
|
||||
return i.get(*args, **kwargs)
|
||||
|
||||
def get(self, key, *args, **kwargs):
|
||||
"""Search for a value in config/key.yaml, config/config.yaml, and env; raise an error if not found"""
|
||||
|
|
@ -118,5 +191,33 @@ class Config(metaclass=Singleton):
|
|||
raise ValueError(f"Key '{key}' not found in environment variables or in the YAML file")
|
||||
return value
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
OPTIONS.get()[name] = value
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
i = OPTIONS.get()
|
||||
return i.get(name)
|
||||
|
||||
def set_context(self, options: dict):
|
||||
"""Update current config"""
|
||||
if not options:
|
||||
return
|
||||
opts = deepcopy(OPTIONS.get())
|
||||
opts.update(options)
|
||||
OPTIONS.set(opts)
|
||||
self._update()
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
"""Return all key-values"""
|
||||
return OPTIONS.get()
|
||||
|
||||
def new_environ(self):
|
||||
"""Return a new os.environ object"""
|
||||
env = os.environ.copy()
|
||||
i = self.options
|
||||
env.update({k: v for k, v in i.items() if isinstance(v, str)})
|
||||
return env
|
||||
|
||||
|
||||
CONFIG = Config()
|
||||
|
|
|
|||
116
metagpt/const.py
116
metagpt/const.py
|
|
@ -4,45 +4,101 @@
|
|||
@Time : 2023/5/1 11:59
|
||||
@Author : alexanderwu
|
||||
@File : const.py
|
||||
@Modified By: mashenquan, 2023-11-1. According to Section 2.2.1 and 2.2.2 of RFC 116, added key definitions for
|
||||
common properties in the Message.
|
||||
@Modified By: mashenquan, 2023-11-27. Defines file repository paths according to Section 2.2.3.4 of RFC 135.
|
||||
@Modified By: mashenquan, 2023/12/5. Add directories for code summarization..
|
||||
"""
|
||||
import contextvars
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
|
||||
def get_project_root():
|
||||
"""Search upwards to find the project root directory."""
|
||||
current_path = Path.cwd()
|
||||
while True:
|
||||
if (
|
||||
(current_path / ".git").exists()
|
||||
or (current_path / ".project_root").exists()
|
||||
or (current_path / ".gitignore").exists()
|
||||
):
|
||||
# use metagpt with git clone will land here
|
||||
logger.info(f"PROJECT_ROOT set to {str(current_path)}")
|
||||
return current_path
|
||||
parent_path = current_path.parent
|
||||
if parent_path == current_path:
|
||||
# use metagpt with pip install will land here
|
||||
cwd = Path.cwd()
|
||||
logger.info(f"PROJECT_ROOT set to current working directory: {str(cwd)}")
|
||||
return cwd
|
||||
current_path = parent_path
|
||||
import metagpt
|
||||
|
||||
OPTIONS = contextvars.ContextVar("OPTIONS", default={})
|
||||
|
||||
|
||||
PROJECT_ROOT = get_project_root()
|
||||
DATA_PATH = PROJECT_ROOT / "data"
|
||||
WORKSPACE_ROOT = PROJECT_ROOT / "workspace"
|
||||
PROMPT_PATH = PROJECT_ROOT / "metagpt/prompts"
|
||||
UT_PATH = PROJECT_ROOT / "data/ut"
|
||||
SWAGGER_PATH = UT_PATH / "files/api/"
|
||||
UT_PY_PATH = UT_PATH / "files/ut/"
|
||||
API_QUESTIONS_PATH = UT_PATH / "files/question/"
|
||||
YAPI_URL = "http://yapi.deepwisdomai.com/"
|
||||
TMP = PROJECT_ROOT / "tmp"
|
||||
def get_metagpt_package_root():
|
||||
"""Get the root directory of the installed package."""
|
||||
package_root = Path(metagpt.__file__).parent.parent
|
||||
for i in (".git", ".project_root", ".gitignore"):
|
||||
if (package_root / i).exists():
|
||||
break
|
||||
else:
|
||||
package_root = Path.cwd()
|
||||
|
||||
logger.info(f"Package root set to {str(package_root)}")
|
||||
return package_root
|
||||
|
||||
|
||||
def get_metagpt_root():
|
||||
"""Get the project root directory."""
|
||||
# Check if a project root is specified in the environment variable
|
||||
project_root_env = os.getenv("METAGPT_PROJECT_ROOT")
|
||||
if project_root_env:
|
||||
project_root = Path(project_root_env)
|
||||
logger.info(f"PROJECT_ROOT set from environment variable to {str(project_root)}")
|
||||
else:
|
||||
# Fallback to package root if no environment variable is set
|
||||
project_root = get_metagpt_package_root()
|
||||
return project_root
|
||||
|
||||
|
||||
# METAGPT PROJECT ROOT AND VARS
|
||||
|
||||
METAGPT_ROOT = get_metagpt_root()
|
||||
DEFAULT_WORKSPACE_ROOT = METAGPT_ROOT / "workspace"
|
||||
|
||||
DATA_PATH = METAGPT_ROOT / "data"
|
||||
RESEARCH_PATH = DATA_PATH / "research"
|
||||
TUTORIAL_PATH = DATA_PATH / "tutorial_docx"
|
||||
INVOICE_OCR_TABLE_PATH = DATA_PATH / "invoice_table"
|
||||
|
||||
SKILL_DIRECTORY = PROJECT_ROOT / "metagpt/skills"
|
||||
UT_PATH = DATA_PATH / "ut"
|
||||
SWAGGER_PATH = UT_PATH / "files/api/"
|
||||
UT_PY_PATH = UT_PATH / "files/ut/"
|
||||
API_QUESTIONS_PATH = UT_PATH / "files/question/"
|
||||
|
||||
SERDESER_PATH = DEFAULT_WORKSPACE_ROOT / "storage" # TODO to store `storage` under the individual generated project
|
||||
|
||||
TMP = METAGPT_ROOT / "tmp"
|
||||
|
||||
SOURCE_ROOT = METAGPT_ROOT / "metagpt"
|
||||
PROMPT_PATH = SOURCE_ROOT / "prompts"
|
||||
SKILL_DIRECTORY = SOURCE_ROOT / "skills"
|
||||
|
||||
|
||||
# REAL CONSTS
|
||||
|
||||
MEM_TTL = 24 * 30 * 3600
|
||||
|
||||
|
||||
MESSAGE_ROUTE_FROM = "sent_from"
|
||||
MESSAGE_ROUTE_TO = "send_to"
|
||||
MESSAGE_ROUTE_CAUSE_BY = "cause_by"
|
||||
MESSAGE_META_ROLE = "role"
|
||||
MESSAGE_ROUTE_TO_ALL = "<all>"
|
||||
MESSAGE_ROUTE_TO_NONE = "<none>"
|
||||
|
||||
REQUIREMENT_FILENAME = "requirement.txt"
|
||||
BUGFIX_FILENAME = "bugfix.txt"
|
||||
PACKAGE_REQUIREMENTS_FILENAME = "requirements.txt"
|
||||
|
||||
DOCS_FILE_REPO = "docs"
|
||||
PRDS_FILE_REPO = "docs/prds"
|
||||
SYSTEM_DESIGN_FILE_REPO = "docs/system_design"
|
||||
TASK_FILE_REPO = "docs/tasks"
|
||||
COMPETITIVE_ANALYSIS_FILE_REPO = "resources/competitive_analysis"
|
||||
DATA_API_DESIGN_FILE_REPO = "resources/data_api_design"
|
||||
SEQ_FLOW_FILE_REPO = "resources/seq_flow"
|
||||
SYSTEM_DESIGN_PDF_FILE_REPO = "resources/system_design"
|
||||
PRD_PDF_FILE_REPO = "resources/prd"
|
||||
TASK_PDF_FILE_REPO = "resources/api_spec_and_tasks"
|
||||
TEST_CODES_FILE_REPO = "tests"
|
||||
TEST_OUTPUTS_FILE_REPO = "test_outputs"
|
||||
CODE_SUMMARIES_FILE_REPO = "docs/code_summaries"
|
||||
CODE_SUMMARIES_PDF_FILE_REPO = "resources/code_summaries"
|
||||
|
||||
YAPI_URL = "http://yapi.deepwisdomai.com/"
|
||||
|
|
|
|||
256
metagpt/document.py
Normal file
256
metagpt/document.py
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/6/8 14:03
|
||||
@Author : alexanderwu
|
||||
@File : document.py
|
||||
@Desc : Classes and Operations Related to Files in the File System.
|
||||
"""
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
|
||||
import pandas as pd
|
||||
from langchain.document_loaders import (
|
||||
TextLoader,
|
||||
UnstructuredPDFLoader,
|
||||
UnstructuredWordDocumentLoader,
|
||||
)
|
||||
from langchain.text_splitter import CharacterTextSplitter
|
||||
from pydantic import BaseModel, Field
|
||||
from tqdm import tqdm
|
||||
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.logs import logger
|
||||
from metagpt.repo_parser import RepoParser
|
||||
|
||||
|
||||
def validate_cols(content_col: str, df: pd.DataFrame):
|
||||
if content_col not in df.columns:
|
||||
raise ValueError("Content column not found in DataFrame.")
|
||||
|
||||
|
||||
def read_data(data_path: Path):
|
||||
suffix = data_path.suffix
|
||||
if ".xlsx" == suffix:
|
||||
data = pd.read_excel(data_path)
|
||||
elif ".csv" == suffix:
|
||||
data = pd.read_csv(data_path)
|
||||
elif ".json" == suffix:
|
||||
data = pd.read_json(data_path)
|
||||
elif suffix in (".docx", ".doc"):
|
||||
data = UnstructuredWordDocumentLoader(str(data_path), mode="elements").load()
|
||||
elif ".txt" == suffix:
|
||||
data = TextLoader(str(data_path)).load()
|
||||
text_splitter = CharacterTextSplitter(separator="\n", chunk_size=256, chunk_overlap=0)
|
||||
texts = text_splitter.split_documents(data)
|
||||
data = texts
|
||||
elif ".pdf" == suffix:
|
||||
data = UnstructuredPDFLoader(str(data_path), mode="elements").load()
|
||||
else:
|
||||
raise NotImplementedError("File format not supported.")
|
||||
return data
|
||||
|
||||
|
||||
class DocumentStatus(Enum):
|
||||
"""Indicates document status, a mechanism similar to RFC/PEP"""
|
||||
|
||||
DRAFT = "draft"
|
||||
UNDERREVIEW = "underreview"
|
||||
APPROVED = "approved"
|
||||
DONE = "done"
|
||||
|
||||
|
||||
class Document(BaseModel):
|
||||
"""
|
||||
Document: Handles operations related to document files.
|
||||
"""
|
||||
|
||||
path: Path = Field(default=None)
|
||||
name: str = Field(default="")
|
||||
content: str = Field(default="")
|
||||
|
||||
# metadata? in content perhaps.
|
||||
author: str = Field(default="")
|
||||
status: DocumentStatus = Field(default=DocumentStatus.DRAFT)
|
||||
reviews: list = Field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_path(cls, path: Path):
|
||||
"""
|
||||
Create a Document instance from a file path.
|
||||
"""
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"File {path} not found.")
|
||||
content = path.read_text()
|
||||
return cls(content=content, path=path)
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str, path: Optional[Path] = None):
|
||||
"""
|
||||
Create a Document from a text string.
|
||||
"""
|
||||
return cls(content=text, path=path)
|
||||
|
||||
def to_path(self, path: Optional[Path] = None):
|
||||
"""
|
||||
Save content to the specified file path.
|
||||
"""
|
||||
if path is not None:
|
||||
self.path = path
|
||||
|
||||
if self.path is None:
|
||||
raise ValueError("File path is not set.")
|
||||
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.path.write_text(self.content, encoding="utf-8")
|
||||
|
||||
def persist(self):
|
||||
"""
|
||||
Persist document to disk.
|
||||
"""
|
||||
return self.to_path()
|
||||
|
||||
|
||||
class IndexableDocument(Document):
|
||||
"""
|
||||
Advanced document handling: For vector databases or search engines.
|
||||
"""
|
||||
|
||||
data: Union[pd.DataFrame, list]
|
||||
content_col: Optional[str] = Field(default="")
|
||||
meta_col: Optional[str] = Field(default="")
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
@classmethod
|
||||
def from_path(cls, data_path: Path, content_col="content", meta_col="metadata"):
|
||||
if not data_path.exists():
|
||||
raise FileNotFoundError(f"File {data_path} not found.")
|
||||
data = read_data(data_path)
|
||||
content = data_path.read_text()
|
||||
if isinstance(data, pd.DataFrame):
|
||||
validate_cols(content_col, data)
|
||||
return cls(data=data, content=content, content_col=content_col, meta_col=meta_col)
|
||||
|
||||
def _get_docs_and_metadatas_by_df(self) -> (list, list):
|
||||
df = self.data
|
||||
docs = []
|
||||
metadatas = []
|
||||
for i in tqdm(range(len(df))):
|
||||
docs.append(df[self.content_col].iloc[i])
|
||||
if self.meta_col:
|
||||
metadatas.append({self.meta_col: df[self.meta_col].iloc[i]})
|
||||
else:
|
||||
metadatas.append({})
|
||||
return docs, metadatas
|
||||
|
||||
def _get_docs_and_metadatas_by_langchain(self) -> (list, list):
|
||||
data = self.data
|
||||
docs = [i.page_content for i in data]
|
||||
metadatas = [i.metadata for i in data]
|
||||
return docs, metadatas
|
||||
|
||||
def get_docs_and_metadatas(self) -> (list, list):
|
||||
if isinstance(self.data, pd.DataFrame):
|
||||
return self._get_docs_and_metadatas_by_df()
|
||||
elif isinstance(self.data, list):
|
||||
return self._get_docs_and_metadatas_by_langchain()
|
||||
else:
|
||||
raise NotImplementedError("Data type not supported for metadata extraction.")
|
||||
|
||||
|
||||
class RepoMetadata(BaseModel):
|
||||
name: str = Field(default="")
|
||||
n_docs: int = Field(default=0)
|
||||
n_chars: int = Field(default=0)
|
||||
symbols: list = Field(default_factory=list)
|
||||
|
||||
|
||||
class Repo(BaseModel):
|
||||
# Name of this repo.
|
||||
name: str = Field(default="")
|
||||
# metadata: RepoMetadata = Field(default=RepoMetadata)
|
||||
docs: dict[Path, Document] = Field(default_factory=dict)
|
||||
codes: dict[Path, Document] = Field(default_factory=dict)
|
||||
assets: dict[Path, Document] = Field(default_factory=dict)
|
||||
path: Path = Field(default=None)
|
||||
|
||||
def _path(self, filename):
|
||||
return self.path / filename
|
||||
|
||||
@classmethod
|
||||
def from_path(cls, path: Path):
|
||||
"""Load documents, code, and assets from a repository path."""
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
repo = Repo(path=path, name=path.name)
|
||||
for file_path in path.rglob("*"):
|
||||
# FIXME: These judgments are difficult to support multiple programming languages and need to be more general
|
||||
if file_path.is_file() and file_path.suffix in [".json", ".txt", ".md", ".py", ".js", ".css", ".html"]:
|
||||
repo._set(file_path.read_text(), file_path)
|
||||
return repo
|
||||
|
||||
def to_path(self):
|
||||
"""Persist all documents, code, and assets to the given repository path."""
|
||||
for doc in self.docs.values():
|
||||
doc.to_path()
|
||||
for code in self.codes.values():
|
||||
code.to_path()
|
||||
for asset in self.assets.values():
|
||||
asset.to_path()
|
||||
|
||||
def _set(self, content: str, path: Path):
|
||||
"""Add a document to the appropriate category based on its file extension."""
|
||||
suffix = path.suffix
|
||||
doc = Document(content=content, path=path, name=str(path.relative_to(self.path)))
|
||||
|
||||
# FIXME: These judgments are difficult to support multiple programming languages and need to be more general
|
||||
if suffix.lower() == ".md":
|
||||
self.docs[path] = doc
|
||||
elif suffix.lower() in [".py", ".js", ".css", ".html"]:
|
||||
self.codes[path] = doc
|
||||
else:
|
||||
self.assets[path] = doc
|
||||
return doc
|
||||
|
||||
def set(self, content: str, filename: str):
|
||||
"""Set a document and persist it to disk."""
|
||||
path = self._path(filename)
|
||||
doc = self._set(content, path)
|
||||
doc.to_path()
|
||||
|
||||
def get(self, filename: str) -> Optional[Document]:
|
||||
"""Get a document by its filename."""
|
||||
path = self._path(filename)
|
||||
return self.docs.get(path) or self.codes.get(path) or self.assets.get(path)
|
||||
|
||||
def get_text_documents(self) -> list[Document]:
|
||||
return list(self.docs.values()) + list(self.codes.values())
|
||||
|
||||
def eda(self) -> RepoMetadata:
|
||||
n_docs = sum(len(i) for i in [self.docs, self.codes, self.assets])
|
||||
n_chars = sum(sum(len(j.content) for j in i.values()) for i in [self.docs, self.codes, self.assets])
|
||||
symbols = RepoParser(base_directory=self.path).generate_symbols()
|
||||
return RepoMetadata(name=self.name, n_docs=n_docs, n_chars=n_chars, symbols=symbols)
|
||||
|
||||
|
||||
def set_existing_repo(path=CONFIG.workspace_path / "t1"):
|
||||
repo1 = Repo.from_path(path)
|
||||
repo1.set("wtf content", "doc/wtf_file.md")
|
||||
repo1.set("wtf code", "code/wtf_file.py")
|
||||
logger.info(repo1) # check doc
|
||||
|
||||
|
||||
def load_existing_repo(path=CONFIG.workspace_path / "web_tetris"):
|
||||
repo = Repo.from_path(path)
|
||||
logger.info(repo)
|
||||
logger.info(repo.eda())
|
||||
|
||||
|
||||
def main():
|
||||
load_existing_repo()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -28,20 +28,20 @@ class BaseStore(ABC):
|
|||
|
||||
|
||||
class LocalStore(BaseStore, ABC):
|
||||
def __init__(self, raw_data: Path, cache_dir: Path = None):
|
||||
if not raw_data:
|
||||
def __init__(self, raw_data_path: Path, cache_dir: Path = None):
|
||||
if not raw_data_path:
|
||||
raise FileNotFoundError
|
||||
self.config = Config()
|
||||
self.raw_data = raw_data
|
||||
self.raw_data_path = raw_data_path
|
||||
if not cache_dir:
|
||||
cache_dir = raw_data.parent
|
||||
cache_dir = raw_data_path.parent
|
||||
self.cache_dir = cache_dir
|
||||
self.store = self._load()
|
||||
if not self.store:
|
||||
self.store = self.write()
|
||||
|
||||
def _get_index_and_store_fname(self):
|
||||
fname = self.raw_data.name.split('.')[0]
|
||||
fname = self.raw_data_path.name.split(".")[0]
|
||||
index_file = self.cache_dir / f"{fname}.index"
|
||||
store_file = self.cache_dir / f"{fname}.pkl"
|
||||
return index_file, store_file
|
||||
|
|
@ -53,4 +53,3 @@ class LocalStore(BaseStore, ABC):
|
|||
@abstractmethod
|
||||
def _write(self, docs, metadatas):
|
||||
raise NotImplementedError
|
||||
|
||||
|
|
@ -10,6 +10,7 @@ import chromadb
|
|||
|
||||
class ChromaStore:
|
||||
"""If inherited from BaseStore, or importing other modules from metagpt, a Python exception occurs, which is strange."""
|
||||
|
||||
def __init__(self, name):
|
||||
client = chromadb.Client()
|
||||
collection = client.create_collection(name)
|
||||
|
|
@ -22,7 +23,7 @@ class ChromaStore:
|
|||
query_texts=[query],
|
||||
n_results=n_results,
|
||||
where=metadata_filter, # optional filter
|
||||
where_document=document_filter # optional filter
|
||||
where_document=document_filter, # optional filter
|
||||
)
|
||||
return results
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
@Time : 2023/6/8 14:03
|
||||
@Author : alexanderwu
|
||||
@File : document.py
|
||||
@Desc : Classes and Operations Related to Vector Files in the Vector Database. Still under design.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -24,20 +25,20 @@ def validate_cols(content_col: str, df: pd.DataFrame):
|
|||
|
||||
def read_data(data_path: Path):
|
||||
suffix = data_path.suffix
|
||||
if '.xlsx' == suffix:
|
||||
if ".xlsx" == suffix:
|
||||
data = pd.read_excel(data_path)
|
||||
elif '.csv' == suffix:
|
||||
elif ".csv" == suffix:
|
||||
data = pd.read_csv(data_path)
|
||||
elif '.json' == suffix:
|
||||
elif ".json" == suffix:
|
||||
data = pd.read_json(data_path)
|
||||
elif suffix in ('.docx', '.doc'):
|
||||
data = UnstructuredWordDocumentLoader(str(data_path), mode='elements').load()
|
||||
elif '.txt' == suffix:
|
||||
elif suffix in (".docx", ".doc"):
|
||||
data = UnstructuredWordDocumentLoader(str(data_path), mode="elements").load()
|
||||
elif ".txt" == suffix:
|
||||
data = TextLoader(str(data_path)).load()
|
||||
text_splitter = CharacterTextSplitter(separator='\n', chunk_size=256, chunk_overlap=0)
|
||||
text_splitter = CharacterTextSplitter(separator="\n", chunk_size=256, chunk_overlap=0)
|
||||
texts = text_splitter.split_documents(data)
|
||||
data = texts
|
||||
elif '.pdf' == suffix:
|
||||
elif ".pdf" == suffix:
|
||||
data = UnstructuredPDFLoader(str(data_path), mode="elements").load()
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
|
@ -45,8 +46,7 @@ def read_data(data_path: Path):
|
|||
|
||||
|
||||
class Document:
|
||||
|
||||
def __init__(self, data_path, content_col='content', meta_col='metadata'):
|
||||
def __init__(self, data_path, content_col="content", meta_col="metadata"):
|
||||
self.data = read_data(data_path)
|
||||
if isinstance(self.data, pd.DataFrame):
|
||||
validate_cols(content_col, self.data)
|
||||
|
|
@ -79,4 +79,3 @@ class Document:
|
|||
return self._get_docs_and_metadatas_by_langchain()
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
@Author : alexanderwu
|
||||
@File : faiss_store.py
|
||||
"""
|
||||
import asyncio
|
||||
import pickle
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
|
@ -14,16 +15,16 @@ from langchain.embeddings import OpenAIEmbeddings
|
|||
from langchain.vectorstores import FAISS
|
||||
|
||||
from metagpt.const import DATA_PATH
|
||||
from metagpt.document import IndexableDocument
|
||||
from metagpt.document_store.base_store import LocalStore
|
||||
from metagpt.document_store.document import Document
|
||||
from metagpt.logs import logger
|
||||
|
||||
|
||||
class FaissStore(LocalStore):
|
||||
def __init__(self, raw_data: Path, cache_dir=None, meta_col='source', content_col='output'):
|
||||
def __init__(self, raw_data_path: Path, cache_dir=None, meta_col="source", content_col="output"):
|
||||
self.meta_col = meta_col
|
||||
self.content_col = content_col
|
||||
super().__init__(raw_data, cache_dir)
|
||||
super().__init__(raw_data_path, cache_dir)
|
||||
|
||||
def _load(self) -> Optional["FaissStore"]:
|
||||
index_file, store_file = self._get_index_and_store_fname()
|
||||
|
|
@ -50,7 +51,7 @@ class FaissStore(LocalStore):
|
|||
pickle.dump(store, f)
|
||||
store.index = index
|
||||
|
||||
def search(self, query, expand_cols=False, sep='\n', *args, k=5, **kwargs):
|
||||
def search(self, query, expand_cols=False, sep="\n", *args, k=5, **kwargs):
|
||||
rsp = self.store.similarity_search(query, k=k, **kwargs)
|
||||
logger.debug(rsp)
|
||||
if expand_cols:
|
||||
|
|
@ -58,11 +59,14 @@ class FaissStore(LocalStore):
|
|||
else:
|
||||
return str(sep.join([f"{x.page_content}" for x in rsp]))
|
||||
|
||||
async def asearch(self, *args, **kwargs):
|
||||
return await asyncio.to_thread(self.search, *args, **kwargs)
|
||||
|
||||
def write(self):
|
||||
"""Initialize the index and library based on the Document (JSON / XLSX, etc.) file provided by the user."""
|
||||
if not self.raw_data.exists():
|
||||
if not self.raw_data_path.exists():
|
||||
raise FileNotFoundError
|
||||
doc = Document(self.raw_data, self.content_col, self.meta_col)
|
||||
doc = IndexableDocument.from_path(self.raw_data_path, self.content_col, self.meta_col)
|
||||
docs, metadatas = doc.get_docs_and_metadatas()
|
||||
|
||||
self.store = self._write(docs, metadatas)
|
||||
|
|
@ -78,8 +82,8 @@ class FaissStore(LocalStore):
|
|||
raise NotImplementedError
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
faiss_store = FaissStore(DATA_PATH / 'qcs/qcs_4w.json')
|
||||
logger.info(faiss_store.search('Oily Skin Facial Cleanser'))
|
||||
faiss_store.add([f'Oily Skin Facial Cleanser-{i}' for i in range(3)])
|
||||
logger.info(faiss_store.search('Oily Skin Facial Cleanser'))
|
||||
if __name__ == "__main__":
|
||||
faiss_store = FaissStore(DATA_PATH / "qcs/qcs_4w.json")
|
||||
logger.info(faiss_store.search("Oily Skin Facial Cleanser"))
|
||||
faiss_store.add([f"Oily Skin Facial Cleanser-{i}" for i in range(3)])
|
||||
logger.info(faiss_store.search("Oily Skin Facial Cleanser"))
|
||||
|
|
|
|||
|
|
@ -12,12 +12,7 @@ from pymilvus import Collection, CollectionSchema, DataType, FieldSchema, connec
|
|||
|
||||
from metagpt.document_store.base_store import BaseStore
|
||||
|
||||
type_mapping = {
|
||||
int: DataType.INT64,
|
||||
str: DataType.VARCHAR,
|
||||
float: DataType.DOUBLE,
|
||||
np.ndarray: DataType.FLOAT_VECTOR
|
||||
}
|
||||
type_mapping = {int: DataType.INT64, str: DataType.VARCHAR, float: DataType.DOUBLE, np.ndarray: DataType.FLOAT_VECTOR}
|
||||
|
||||
|
||||
def columns_to_milvus_schema(columns: dict, primary_col_name: str = "", desc: str = ""):
|
||||
|
|
@ -52,17 +47,11 @@ class MilvusStore(BaseStore):
|
|||
self.collection = None
|
||||
|
||||
def _create_collection(self, name, schema):
|
||||
collection = Collection(
|
||||
name=name,
|
||||
schema=schema,
|
||||
using='default',
|
||||
shards_num=2,
|
||||
consistency_level="Strong"
|
||||
)
|
||||
collection = Collection(name=name, schema=schema, using="default", shards_num=2, consistency_level="Strong")
|
||||
return collection
|
||||
|
||||
def create_collection(self, name, columns):
|
||||
schema = columns_to_milvus_schema(columns, 'idx')
|
||||
schema = columns_to_milvus_schema(columns, "idx")
|
||||
self.collection = self._create_collection(name, schema)
|
||||
return self.collection
|
||||
|
||||
|
|
@ -72,7 +61,7 @@ class MilvusStore(BaseStore):
|
|||
def load_collection(self):
|
||||
self.collection.load()
|
||||
|
||||
def build_index(self, field='emb'):
|
||||
def build_index(self, field="emb"):
|
||||
self.collection.create_index(field, {"index_type": "FLAT", "metric_type": "L2", "params": {}})
|
||||
|
||||
def search(self, query: list[list[float]], *args, **kwargs):
|
||||
|
|
@ -85,11 +74,11 @@ class MilvusStore(BaseStore):
|
|||
search_params = {"metric_type": "L2", "params": {"nprobe": 10}}
|
||||
results = self.collection.search(
|
||||
data=query,
|
||||
anns_field=kwargs.get('field', 'emb'),
|
||||
anns_field=kwargs.get("field", "emb"),
|
||||
param=search_params,
|
||||
limit=10,
|
||||
expr=None,
|
||||
consistency_level="Strong"
|
||||
consistency_level="Strong",
|
||||
)
|
||||
# FIXME: results contain id, but to get the actual value from the id, we still need to call the query interface
|
||||
return results
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ from metagpt.document_store.base_store import BaseStore
|
|||
@dataclass
|
||||
class QdrantConnection:
|
||||
"""
|
||||
Args:
|
||||
url: qdrant url
|
||||
host: qdrant host
|
||||
port: qdrant port
|
||||
memory: qdrant service use memory mode
|
||||
api_key: qdrant cloud api_key
|
||||
"""
|
||||
Args:
|
||||
url: qdrant url
|
||||
host: qdrant host
|
||||
port: qdrant port
|
||||
memory: qdrant service use memory mode
|
||||
api_key: qdrant cloud api_key
|
||||
"""
|
||||
|
||||
url: str = None
|
||||
host: str = None
|
||||
port: int = None
|
||||
|
|
@ -31,9 +32,7 @@ class QdrantStore(BaseStore):
|
|||
elif connect.url:
|
||||
self.client = QdrantClient(url=connect.url, api_key=connect.api_key)
|
||||
elif connect.host and connect.port:
|
||||
self.client = QdrantClient(
|
||||
host=connect.host, port=connect.port, api_key=connect.api_key
|
||||
)
|
||||
self.client = QdrantClient(host=connect.host, port=connect.port, api_key=connect.api_key)
|
||||
else:
|
||||
raise Exception("please check QdrantConnection.")
|
||||
|
||||
|
|
@ -58,15 +57,11 @@ class QdrantStore(BaseStore):
|
|||
try:
|
||||
self.client.get_collection(collection_name)
|
||||
if force_recreate:
|
||||
res = self.client.recreate_collection(
|
||||
collection_name, vectors_config=vectors_config, **kwargs
|
||||
)
|
||||
res = self.client.recreate_collection(collection_name, vectors_config=vectors_config, **kwargs)
|
||||
return res
|
||||
return True
|
||||
except: # noqa: E722
|
||||
return self.client.recreate_collection(
|
||||
collection_name, vectors_config=vectors_config, **kwargs
|
||||
)
|
||||
return self.client.recreate_collection(collection_name, vectors_config=vectors_config, **kwargs)
|
||||
|
||||
def has_collection(self, collection_name: str):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -4,60 +4,132 @@
|
|||
@Time : 2023/5/11 22:12
|
||||
@Author : alexanderwu
|
||||
@File : environment.py
|
||||
@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.2 of RFC 116:
|
||||
1. Remove the functionality of `Environment` class as a public message buffer.
|
||||
2. Standardize the message forwarding behavior of the `Environment` class.
|
||||
3. Add the `is_idle` property.
|
||||
@Modified By: mashenquan, 2023-11-4. According to the routing feature plan in Chapter 2.2.3.2 of RFC 113, the routing
|
||||
functionality is to be consolidated into the `Environment` class.
|
||||
"""
|
||||
import asyncio
|
||||
from typing import Iterable
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Set
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from metagpt.memory import Memory
|
||||
from metagpt.roles import Role
|
||||
from metagpt.logs import logger
|
||||
from metagpt.roles.role import Role, role_subclass_registry
|
||||
from metagpt.schema import Message
|
||||
from metagpt.utils.common import is_subscribed, read_json_file, write_json_file
|
||||
|
||||
|
||||
class Environment(BaseModel):
|
||||
"""环境,承载一批角色,角色可以向环境发布消息,可以被其他角色观察到
|
||||
Environment, hosting a batch of roles, roles can publish messages to the environment, and can be observed by other roles
|
||||
|
||||
Environment, hosting a batch of roles, roles can publish messages to the environment, and can be observed by other roles
|
||||
"""
|
||||
|
||||
roles: dict[str, Role] = Field(default_factory=dict)
|
||||
memory: Memory = Field(default_factory=Memory)
|
||||
history: str = Field(default='')
|
||||
members: dict[Role, Set] = Field(default_factory=dict)
|
||||
history: str = "" # For debug
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
roles = []
|
||||
for role_key, role in kwargs.get("roles", {}).items():
|
||||
current_role = kwargs["roles"][role_key]
|
||||
if isinstance(current_role, dict):
|
||||
item_class_name = current_role.get("builtin_class_name", None)
|
||||
for name, subclass in role_subclass_registry.items():
|
||||
registery_class_name = subclass.__fields__["builtin_class_name"].default
|
||||
if item_class_name == registery_class_name:
|
||||
current_role = subclass(**current_role)
|
||||
break
|
||||
kwargs["roles"][role_key] = current_role
|
||||
roles.append(current_role)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.add_roles(roles) # add_roles again to init the Role.set_env
|
||||
|
||||
def serialize(self, stg_path: Path):
|
||||
roles_path = stg_path.joinpath("roles.json")
|
||||
roles_info = []
|
||||
for role_key, role in self.roles.items():
|
||||
roles_info.append(
|
||||
{
|
||||
"role_class": role.__class__.__name__,
|
||||
"module_name": role.__module__,
|
||||
"role_name": role.name,
|
||||
"role_sub_tags": list(self.members.get(role)),
|
||||
}
|
||||
)
|
||||
role.serialize(stg_path=stg_path.joinpath(f"roles/{role.__class__.__name__}_{role.name}"))
|
||||
write_json_file(roles_path, roles_info)
|
||||
|
||||
history_path = stg_path.joinpath("history.json")
|
||||
write_json_file(history_path, {"content": self.history})
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, stg_path: Path) -> "Environment":
|
||||
"""stg_path: ./storage/team/environment/"""
|
||||
roles_path = stg_path.joinpath("roles.json")
|
||||
roles_info = read_json_file(roles_path)
|
||||
roles = []
|
||||
for role_info in roles_info:
|
||||
# role stored in ./environment/roles/{role_class}_{role_name}
|
||||
role_path = stg_path.joinpath(f"roles/{role_info.get('role_class')}_{role_info.get('role_name')}")
|
||||
role = Role.deserialize(role_path)
|
||||
roles.append(role)
|
||||
|
||||
history = read_json_file(stg_path.joinpath("history.json"))
|
||||
history = history.get("content")
|
||||
|
||||
environment = Environment(**{"history": history})
|
||||
environment.add_roles(roles)
|
||||
|
||||
return environment
|
||||
|
||||
def add_role(self, role: Role):
|
||||
"""增加一个在当前环境的角色
|
||||
Add a role in the current environment
|
||||
Add a role in the current environment
|
||||
"""
|
||||
role.set_env(self)
|
||||
self.roles[role.profile] = role
|
||||
|
||||
def add_roles(self, roles: Iterable[Role]):
|
||||
"""增加一批在当前环境的角色
|
||||
Add a batch of characters in the current environment
|
||||
Add a batch of characters in the current environment
|
||||
"""
|
||||
for role in roles:
|
||||
self.add_role(role)
|
||||
|
||||
def publish_message(self, message: Message):
|
||||
"""向当前环境发布信息
|
||||
Post information to the current environment
|
||||
def publish_message(self, message: Message) -> bool:
|
||||
"""
|
||||
# self.message_queue.put(message)
|
||||
self.memory.add(message)
|
||||
self.history += f"\n{message}"
|
||||
Distribute the message to the recipients.
|
||||
In accordance with the Message routing structure design in Chapter 2.2.1 of RFC 116, as already planned
|
||||
in RFC 113 for the entire system, the routing information in the Message is only responsible for
|
||||
specifying the message recipient, without concern for where the message recipient is located. How to
|
||||
route the message to the message recipient is a problem addressed by the transport framework designed
|
||||
in RFC 113.
|
||||
"""
|
||||
logger.debug(f"publish_message: {message.dump()}")
|
||||
found = False
|
||||
# According to the routing feature plan in Chapter 2.2.3.2 of RFC 113
|
||||
for role, subscription in self.members.items():
|
||||
if is_subscribed(message, subscription):
|
||||
role.put_message(message)
|
||||
found = True
|
||||
if not found:
|
||||
logger.warning(f"Message no recipients: {message.dump()}")
|
||||
self.history += f"\n{message}" # For debug
|
||||
|
||||
return True
|
||||
|
||||
async def run(self, k=1):
|
||||
"""处理一次所有信息的运行
|
||||
Process all Role runs at once
|
||||
"""
|
||||
# while not self.message_queue.empty():
|
||||
# message = self.message_queue.get()
|
||||
# rsp = await self.manager.handle(message, self)
|
||||
# self.message_queue.put(rsp)
|
||||
for _ in range(k):
|
||||
futures = []
|
||||
for role in self.roles.values():
|
||||
|
|
@ -65,15 +137,32 @@ class Environment(BaseModel):
|
|||
futures.append(future)
|
||||
|
||||
await asyncio.gather(*futures)
|
||||
logger.debug(f"is idle: {self.is_idle}")
|
||||
|
||||
def get_roles(self) -> dict[str, Role]:
|
||||
"""获得环境内的所有角色
|
||||
Process all Role runs at once
|
||||
Process all Role runs at once
|
||||
"""
|
||||
return self.roles
|
||||
|
||||
def get_role(self, name: str) -> Role:
|
||||
"""获得环境内的指定角色
|
||||
get all the environment roles
|
||||
get all the environment roles
|
||||
"""
|
||||
return self.roles.get(name, None)
|
||||
|
||||
@property
|
||||
def is_idle(self):
|
||||
"""If true, all actions have been executed."""
|
||||
for r in self.roles.values():
|
||||
if not r.is_idle:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_subscription(self, obj):
|
||||
"""Get the labels for messages to be consumed by the object."""
|
||||
return self.members.get(obj, {})
|
||||
|
||||
def set_subscription(self, obj, tags):
|
||||
"""Set the labels for message to be consumed by the object"""
|
||||
self.members[obj] = tags
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/5/28 14:54
|
||||
@Author : alexanderwu
|
||||
@File : inspect_module.py
|
||||
"""
|
||||
|
||||
import inspect
|
||||
|
||||
import metagpt # replace with your module
|
||||
|
||||
|
||||
def print_classes_and_functions(module):
|
||||
"""FIXME: NOT WORK.. """
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if inspect.isclass(obj):
|
||||
print(f'Class: {name}')
|
||||
elif inspect.isfunction(obj):
|
||||
print(f'Function: {name}')
|
||||
else:
|
||||
print(name)
|
||||
|
||||
print(dir(module))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print_classes_and_functions(metagpt)
|
||||
|
|
@ -6,27 +6,14 @@
|
|||
@File : llm.py
|
||||
"""
|
||||
|
||||
from metagpt.logs import logger
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.provider.anthropic_api import Claude2 as Claude
|
||||
from metagpt.provider.openai_api import OpenAIGPTAPI
|
||||
from metagpt.provider.zhipuai_api import ZhiPuAIGPTAPI
|
||||
from metagpt.provider.spark_api import SparkAPI
|
||||
from metagpt.config import CONFIG, LLMProviderEnum
|
||||
from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
from metagpt.provider.human_provider import HumanProvider
|
||||
from metagpt.provider.llm_provider_registry import LLM_REGISTRY
|
||||
|
||||
_ = HumanProvider() # Avoid pre-commit error
|
||||
|
||||
|
||||
def LLM() -> "BaseGPTAPI":
|
||||
""" initialize different LLM instance according to the key field existence"""
|
||||
# TODO a little trick, can use registry to initialize LLM instance further
|
||||
if CONFIG.openai_api_key:
|
||||
llm = OpenAIGPTAPI()
|
||||
elif CONFIG.claude_api_key:
|
||||
llm = Claude()
|
||||
elif CONFIG.spark_api_key:
|
||||
llm = SparkAPI()
|
||||
elif CONFIG.zhipuai_api_key:
|
||||
llm = ZhiPuAIGPTAPI()
|
||||
else:
|
||||
raise RuntimeError("You should config a LLM configuration first")
|
||||
|
||||
return llm
|
||||
def LLM(provider: LLMProviderEnum = CONFIG.get_default_llm_provider_enum()) -> BaseGPTAPI:
|
||||
"""get the default llm provider"""
|
||||
return LLM_REGISTRY.get_provider(provider)
|
||||
|
|
|
|||
|
|
@ -7,18 +7,22 @@
|
|||
"""
|
||||
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
from loguru import logger as _logger
|
||||
|
||||
from metagpt.const import PROJECT_ROOT
|
||||
from metagpt.const import METAGPT_ROOT
|
||||
|
||||
|
||||
def define_log_level(print_level="INFO", logfile_level="DEBUG"):
|
||||
"""调整日志级别到level之上
|
||||
Adjust the log level to above level
|
||||
"""
|
||||
"""Adjust the log level to above level"""
|
||||
current_date = datetime.now()
|
||||
formatted_date = current_date.strftime("%Y%m%d")
|
||||
|
||||
_logger.remove()
|
||||
_logger.add(sys.stderr, level=print_level)
|
||||
_logger.add(PROJECT_ROOT / 'logs/log.txt', level=logfile_level)
|
||||
_logger.add(METAGPT_ROOT / f"logs/{formatted_date}.txt", level=logfile_level)
|
||||
return _logger
|
||||
|
||||
|
||||
logger = define_log_level()
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ class SkillManager:
|
|||
|
||||
def __init__(self):
|
||||
self._llm = LLM()
|
||||
self._store = ChromaStore('skill_manager')
|
||||
self._skills: dict[str: Skill] = {}
|
||||
self._store = ChromaStore("skill_manager")
|
||||
self._skills: dict[str:Skill] = {}
|
||||
|
||||
def add_skill(self, skill: Skill):
|
||||
"""
|
||||
|
|
@ -54,7 +54,7 @@ class SkillManager:
|
|||
:param desc: Skill description
|
||||
:return: Multiple skills
|
||||
"""
|
||||
return self._store.search(desc, n_results=n_results)['ids'][0]
|
||||
return self._store.search(desc, n_results=n_results)["ids"][0]
|
||||
|
||||
def retrieve_skill_scored(self, desc: str, n_results: int = 2) -> dict:
|
||||
"""
|
||||
|
|
@ -75,6 +75,6 @@ class SkillManager:
|
|||
logger.info(text)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
manager = SkillManager()
|
||||
manager.generate_skill_desc(Action())
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/5/11 14:42
|
||||
@Author : alexanderwu
|
||||
@File : manager.py
|
||||
"""
|
||||
from metagpt.llm import LLM
|
||||
from metagpt.logs import logger
|
||||
from metagpt.schema import Message
|
||||
|
||||
|
||||
class Manager:
|
||||
def __init__(self, llm: LLM = LLM()):
|
||||
self.llm = llm # Large Language Model
|
||||
self.role_directions = {
|
||||
"BOSS": "Product Manager",
|
||||
"Product Manager": "Architect",
|
||||
"Architect": "Engineer",
|
||||
"Engineer": "QA Engineer",
|
||||
"QA Engineer": "Product Manager"
|
||||
}
|
||||
self.prompt_template = """
|
||||
Given the following message:
|
||||
{message}
|
||||
|
||||
And the current status of roles:
|
||||
{roles}
|
||||
|
||||
Which role should handle this message?
|
||||
"""
|
||||
|
||||
async def handle(self, message: Message, environment):
|
||||
"""
|
||||
管理员处理信息,现在简单的将信息递交给下一个人
|
||||
The administrator processes the information, now simply passes the information on to the next person
|
||||
:param message:
|
||||
:param environment:
|
||||
:return:
|
||||
"""
|
||||
# Get all roles from the environment
|
||||
roles = environment.get_roles()
|
||||
# logger.debug(f"{roles=}, {message=}")
|
||||
|
||||
# Build a context for the LLM to understand the situation
|
||||
# context = {
|
||||
# "message": str(message),
|
||||
# "roles": {role.name: role.get_info() for role in roles},
|
||||
# }
|
||||
# Ask the LLM to decide which role should handle the message
|
||||
# chosen_role_name = self.llm.ask(self.prompt_template.format(context))
|
||||
|
||||
# FIXME: 现在通过简单的字典决定流向,但之后还是应该有思考过程
|
||||
#The direction of flow is now determined by a simple dictionary, but there should still be a thought process afterwards
|
||||
next_role_profile = self.role_directions[message.role]
|
||||
# logger.debug(f"{next_role_profile}")
|
||||
for _, role in roles.items():
|
||||
if next_role_profile == role.profile:
|
||||
next_role = role
|
||||
break
|
||||
else:
|
||||
logger.error(f"No available role can handle message: {message}.")
|
||||
return
|
||||
|
||||
# Find the chosen role and handle the message
|
||||
return await next_role.handle(message)
|
||||
|
|
@ -7,10 +7,11 @@
|
|||
"""
|
||||
|
||||
from metagpt.memory.memory import Memory
|
||||
from metagpt.memory.longterm_memory import LongTermMemory
|
||||
|
||||
# from metagpt.memory.longterm_memory import LongTermMemory
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Memory",
|
||||
"LongTermMemory",
|
||||
# "LongTermMemory",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Desc : the implement of Long-term memory
|
||||
"""
|
||||
@Desc : the implement of Long-term memory
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from metagpt.logs import logger
|
||||
from metagpt.memory import Memory
|
||||
|
|
@ -15,11 +21,12 @@ class LongTermMemory(Memory):
|
|||
- update memory when it changed
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.memory_storage: MemoryStorage = MemoryStorage()
|
||||
super(LongTermMemory, self).__init__()
|
||||
self.rc = None # RoleContext
|
||||
self.msg_from_recover = False
|
||||
memory_storage: MemoryStorage = Field(default_factory=MemoryStorage)
|
||||
rc: Optional["RoleContext"] = None
|
||||
msg_from_recover: bool = False
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def recover_memory(self, role_id: str, rc: "RoleContext"):
|
||||
messages = self.memory_storage.recover_memory(role_id)
|
||||
|
|
@ -28,14 +35,14 @@ class LongTermMemory(Memory):
|
|||
logger.warning(f"It may the first time to run Agent {role_id}, the long-term memory is empty")
|
||||
else:
|
||||
logger.warning(
|
||||
f"Agent {role_id} has existed memory storage with {len(messages)} messages " f"and has recovered them."
|
||||
f"Agent {role_id} has existing memory storage with {len(messages)} messages " f"and has recovered them."
|
||||
)
|
||||
self.msg_from_recover = True
|
||||
self.add_batch(messages)
|
||||
self.msg_from_recover = False
|
||||
|
||||
def add(self, message: Message):
|
||||
super(LongTermMemory, self).add(message)
|
||||
super().add(message)
|
||||
for action in self.rc.watch:
|
||||
if message.cause_by == action and not self.msg_from_recover:
|
||||
# currently, only add role's watching messages to its memory_storage
|
||||
|
|
@ -48,7 +55,7 @@ class LongTermMemory(Memory):
|
|||
1. find the short-term memory(stm) news
|
||||
2. furthermore, filter out similar messages based on ltm(long-term memory), get the final news
|
||||
"""
|
||||
stm_news = super(LongTermMemory, self).find_news(observed, k=k) # shot-term memory news
|
||||
stm_news = super().find_news(observed, k=k) # shot-term memory news
|
||||
if not self.memory_storage.is_initialized:
|
||||
# memory_storage hasn't initialized, use default `find_news` to get stm_news
|
||||
return stm_news
|
||||
|
|
@ -62,10 +69,9 @@ class LongTermMemory(Memory):
|
|||
return ltm_news[-k:]
|
||||
|
||||
def delete(self, message: Message):
|
||||
super(LongTermMemory, self).delete(message)
|
||||
super().delete(message)
|
||||
# TODO delete message in memory_storage
|
||||
|
||||
def clear(self):
|
||||
super(LongTermMemory, self).clear()
|
||||
super().clear()
|
||||
self.memory_storage.clean()
|
||||
|
||||
|
|
@ -4,21 +4,53 @@
|
|||
@Time : 2023/5/20 12:15
|
||||
@Author : alexanderwu
|
||||
@File : memory.py
|
||||
@Modified By: mashenquan, 2023-11-1. According to RFC 116: Updated the type of index key.
|
||||
"""
|
||||
from collections import defaultdict
|
||||
from typing import Iterable, Type
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Set
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from metagpt.actions import Action
|
||||
from metagpt.schema import Message
|
||||
from metagpt.utils.common import (
|
||||
any_to_str,
|
||||
any_to_str_set,
|
||||
read_json_file,
|
||||
write_json_file,
|
||||
)
|
||||
|
||||
|
||||
class Memory:
|
||||
class Memory(BaseModel):
|
||||
"""The most basic memory: super-memory"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize an empty storage list and an empty index dictionary"""
|
||||
self.storage: list[Message] = []
|
||||
self.index: dict[Type[Action], list[Message]] = defaultdict(list)
|
||||
storage: list[Message] = []
|
||||
index: dict[str, list[Message]] = Field(default_factory=defaultdict(list))
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
index = kwargs.get("index", {})
|
||||
new_index = defaultdict(list)
|
||||
for action_str, value in index.items():
|
||||
new_index[action_str] = [Message(**item_dict) for item_dict in value]
|
||||
kwargs["index"] = new_index
|
||||
super(Memory, self).__init__(**kwargs)
|
||||
self.index = new_index
|
||||
|
||||
def serialize(self, stg_path: Path):
|
||||
"""stg_path = ./storage/team/environment/ or ./storage/team/environment/roles/{role_class}_{role_name}/"""
|
||||
memory_path = stg_path.joinpath("memory.json")
|
||||
storage = self.dict()
|
||||
write_json_file(memory_path, storage)
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, stg_path: Path) -> "Memory":
|
||||
"""stg_path = ./storage/team/environment/ or ./storage/team/environment/roles/{role_class}_{role_name}/"""
|
||||
memory_path = stg_path.joinpath("memory.json")
|
||||
|
||||
memory_dict = read_json_file(memory_path)
|
||||
memory = Memory(**memory_dict)
|
||||
|
||||
return memory
|
||||
|
||||
def add(self, message: Message):
|
||||
"""Add a new message to storage, while updating the index"""
|
||||
|
|
@ -40,6 +72,16 @@ class Memory:
|
|||
"""Return all messages containing a specified content"""
|
||||
return [message for message in self.storage if content in message.content]
|
||||
|
||||
def delete_newest(self) -> "Message":
|
||||
"""delete the newest message from the storage"""
|
||||
if len(self.storage) > 0:
|
||||
newest_msg = self.storage.pop()
|
||||
if newest_msg.cause_by and newest_msg in self.index[newest_msg.cause_by]:
|
||||
self.index[newest_msg.cause_by].remove(newest_msg)
|
||||
else:
|
||||
newest_msg = None
|
||||
return newest_msg
|
||||
|
||||
def delete(self, message: Message):
|
||||
"""Delete the specified message from storage, while updating the index"""
|
||||
self.storage.remove(message)
|
||||
|
|
@ -73,16 +115,17 @@ class Memory:
|
|||
news.append(i)
|
||||
return news
|
||||
|
||||
def get_by_action(self, action: Type[Action]) -> list[Message]:
|
||||
def get_by_action(self, action) -> list[Message]:
|
||||
"""Return all messages triggered by a specified Action"""
|
||||
return self.index[action]
|
||||
index = any_to_str(action)
|
||||
return self.index[index]
|
||||
|
||||
def get_by_actions(self, actions: Iterable[Type[Action]]) -> list[Message]:
|
||||
def get_by_actions(self, actions: Set) -> list[Message]:
|
||||
"""Return all messages triggered by specified Actions"""
|
||||
rsp = []
|
||||
for action in actions:
|
||||
indices = any_to_str_set(actions)
|
||||
for action in indices:
|
||||
if action not in self.index:
|
||||
continue
|
||||
rsp += self.index[action]
|
||||
return rsp
|
||||
|
||||
|
|
@ -2,16 +2,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# @Desc : the implement of memory storage
|
||||
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from langchain.vectorstores.faiss import FAISS
|
||||
|
||||
from metagpt.const import DATA_PATH, MEM_TTL
|
||||
from metagpt.document_store.faiss_store import FaissStore
|
||||
from metagpt.logs import logger
|
||||
from metagpt.schema import Message
|
||||
from metagpt.utils.serialize import serialize_message, deserialize_message
|
||||
from metagpt.document_store.faiss_store import FaissStore
|
||||
from metagpt.utils.serialize import deserialize_message, serialize_message
|
||||
|
||||
|
||||
class MemoryStorage(FaissStore):
|
||||
|
|
@ -34,7 +34,7 @@ class MemoryStorage(FaissStore):
|
|||
|
||||
def recover_memory(self, role_id: str) -> List[Message]:
|
||||
self.role_id = role_id
|
||||
self.role_mem_path = Path(DATA_PATH / f'role_mem/{self.role_id}/')
|
||||
self.role_mem_path = Path(DATA_PATH / f"role_mem/{self.role_id}/")
|
||||
self.role_mem_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self.store = self._load()
|
||||
|
|
@ -51,18 +51,18 @@ class MemoryStorage(FaissStore):
|
|||
|
||||
def _get_index_and_store_fname(self):
|
||||
if not self.role_mem_path:
|
||||
logger.error(f'You should call {self.__class__.__name__}.recover_memory fist when using LongTermMemory')
|
||||
logger.error(f"You should call {self.__class__.__name__}.recover_memory fist when using LongTermMemory")
|
||||
return None, None
|
||||
index_fpath = Path(self.role_mem_path / f'{self.role_id}.index')
|
||||
storage_fpath = Path(self.role_mem_path / f'{self.role_id}.pkl')
|
||||
index_fpath = Path(self.role_mem_path / f"{self.role_id}.index")
|
||||
storage_fpath = Path(self.role_mem_path / f"{self.role_id}.pkl")
|
||||
return index_fpath, storage_fpath
|
||||
|
||||
def persist(self):
|
||||
super(MemoryStorage, self).persist()
|
||||
logger.debug(f'Agent {self.role_id} persist memory into local')
|
||||
super().persist()
|
||||
logger.debug(f"Agent {self.role_id} persist memory into local")
|
||||
|
||||
def add(self, message: Message) -> bool:
|
||||
""" add message into memory storage"""
|
||||
"""add message into memory storage"""
|
||||
docs = [message.content]
|
||||
metadatas = [{"message_ser": serialize_message(message)}]
|
||||
if not self.store:
|
||||
|
|
@ -79,10 +79,7 @@ class MemoryStorage(FaissStore):
|
|||
if not self.store:
|
||||
return []
|
||||
|
||||
resp = self.store.similarity_search_with_score(
|
||||
query=message.content,
|
||||
k=k
|
||||
)
|
||||
resp = self.store.similarity_search_with_score(query=message.content, k=k)
|
||||
# filter the result which score is smaller than the threshold
|
||||
filtered_resp = []
|
||||
for item, score in resp:
|
||||
|
|
@ -104,4 +101,3 @@ class MemoryStorage(FaissStore):
|
|||
|
||||
self.store = None
|
||||
self._initialized = False
|
||||
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
from typing import Optional
|
||||
from abc import ABC
|
||||
from metagpt.llm import LLM # Large language model, similar to GPT
|
||||
n
|
||||
|
||||
class Action(ABC):
|
||||
def __init__(self, name='', context=None, llm: LLM = LLM()):
|
||||
self.name = name
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@
|
|||
|
||||
COMMON_PROMPT = "Now I will provide you with the OCR text recognition results for the invoice."
|
||||
|
||||
EXTRACT_OCR_MAIN_INFO_PROMPT = COMMON_PROMPT + """
|
||||
EXTRACT_OCR_MAIN_INFO_PROMPT = (
|
||||
COMMON_PROMPT
|
||||
+ """
|
||||
Please extract the payee, city, total cost, and invoicing date of the invoice.
|
||||
|
||||
The OCR data of the invoice are as follows:
|
||||
|
|
@ -22,8 +24,11 @@ Mandatory restrictions are returned according to the following requirements:
|
|||
2. The returned JSON dictionary must be returned in {language}
|
||||
3. Mandatory requirement to output in JSON format: {{"收款人":"x","城市":"x","总费用/元":"","开票日期":""}}.
|
||||
"""
|
||||
)
|
||||
|
||||
REPLY_OCR_QUESTION_PROMPT = COMMON_PROMPT + """
|
||||
REPLY_OCR_QUESTION_PROMPT = (
|
||||
COMMON_PROMPT
|
||||
+ """
|
||||
Please answer the question: {query}
|
||||
|
||||
The OCR data of the invoice are as follows:
|
||||
|
|
@ -34,6 +39,6 @@ Mandatory restrictions are returned according to the following requirements:
|
|||
2. Enforce restrictions on not returning OCR data sent to you.
|
||||
3. Return with markdown syntax layout.
|
||||
"""
|
||||
)
|
||||
|
||||
INVOICE_OCR_SUCCESS = "Successfully completed OCR text recognition invoice."
|
||||
|
||||
|
|
|
|||
|
|
@ -54,10 +54,12 @@ Conversation history:
|
|||
{salesperson_name}:
|
||||
"""
|
||||
|
||||
conversation_stages = {'1' : "Introduction: Start the conversation by introducing yourself and your company. Be polite and respectful while keeping the tone of the conversation professional. Your greeting should be welcoming. Always clarify in your greeting the reason why you are contacting the prospect.",
|
||||
'2': "Qualification: Qualify the prospect by confirming if they are the right person to talk to regarding your product/service. Ensure that they have the authority to make purchasing decisions.",
|
||||
'3': "Value proposition: Briefly explain how your product/service can benefit the prospect. Focus on the unique selling points and value proposition of your product/service that sets it apart from competitors.",
|
||||
'4': "Needs analysis: Ask open-ended questions to uncover the prospect's needs and pain points. Listen carefully to their responses and take notes.",
|
||||
'5': "Solution presentation: Based on the prospect's needs, present your product/service as the solution that can address their pain points.",
|
||||
'6': "Objection handling: Address any objections that the prospect may have regarding your product/service. Be prepared to provide evidence or testimonials to support your claims.",
|
||||
'7': "Close: Ask for the sale by proposing a next step. This could be a demo, a trial or a meeting with decision-makers. Ensure to summarize what has been discussed and reiterate the benefits."}
|
||||
conversation_stages = {
|
||||
"1": "Introduction: Start the conversation by introducing yourself and your company. Be polite and respectful while keeping the tone of the conversation professional. Your greeting should be welcoming. Always clarify in your greeting the reason why you are contacting the prospect.",
|
||||
"2": "Qualification: Qualify the prospect by confirming if they are the right person to talk to regarding your product/service. Ensure that they have the authority to make purchasing decisions.",
|
||||
"3": "Value proposition: Briefly explain how your product/service can benefit the prospect. Focus on the unique selling points and value proposition of your product/service that sets it apart from competitors.",
|
||||
"4": "Needs analysis: Ask open-ended questions to uncover the prospect's needs and pain points. Listen carefully to their responses and take notes.",
|
||||
"5": "Solution presentation: Based on the prospect's needs, present your product/service as the solution that can address their pain points.",
|
||||
"6": "Objection handling: Address any objections that the prospect may have regarding your product/service. Be prepared to provide evidence or testimonials to support your claims.",
|
||||
"7": "Close: Ask for the sale by proposing a next step. This could be a demo, a trial or a meeting with decision-makers. Ensure to summarize what has been discussed and reiterate the benefits.",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ You are now a seasoned technical professional in the field of the internet.
|
|||
We need you to write a technical tutorial with the topic "{topic}".
|
||||
"""
|
||||
|
||||
DIRECTORY_PROMPT = COMMON_PROMPT + """
|
||||
DIRECTORY_PROMPT = (
|
||||
COMMON_PROMPT
|
||||
+ """
|
||||
Please provide the specific table of contents for this tutorial, strictly following the following requirements:
|
||||
1. The output must be strictly in the specified language, {language}.
|
||||
2. Answer strictly in the dictionary format like {{"title": "xxx", "directory": [{{"dir 1": ["sub dir 1", "sub dir 2"]}}, {{"dir 2": ["sub dir 3", "sub dir 4"]}}]}}.
|
||||
|
|
@ -20,8 +22,11 @@ Please provide the specific table of contents for this tutorial, strictly follow
|
|||
4. Do not have extra spaces or line breaks.
|
||||
5. Each directory title has practical significance.
|
||||
"""
|
||||
)
|
||||
|
||||
CONTENT_PROMPT = COMMON_PROMPT + """
|
||||
CONTENT_PROMPT = (
|
||||
COMMON_PROMPT
|
||||
+ """
|
||||
Now I will give you the module directory titles for the topic.
|
||||
Please output the detailed principle content of this title in detail.
|
||||
If there are code examples, please provide them according to standard code specifications.
|
||||
|
|
@ -36,4 +41,5 @@ Strictly limit output according to the following requirements:
|
|||
3. The output must be strictly in the specified language, {language}.
|
||||
4. Do not have redundant output, including concluding remarks.
|
||||
5. Strict requirement not to output the topic "{topic}".
|
||||
"""
|
||||
"""
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from metagpt.config import CONFIG
|
|||
|
||||
class Claude2:
|
||||
def ask(self, prompt):
|
||||
client = Anthropic(api_key=CONFIG.claude_api_key)
|
||||
client = Anthropic(api_key=CONFIG.anthropic_api_key)
|
||||
|
||||
res = client.completions.create(
|
||||
model="claude-2",
|
||||
|
|
@ -24,7 +24,7 @@ class Claude2:
|
|||
return res.completion
|
||||
|
||||
async def aask(self, prompt):
|
||||
client = Anthropic(api_key=CONFIG.claude_api_key)
|
||||
client = Anthropic(api_key=CONFIG.anthropic_api_key)
|
||||
|
||||
res = client.completions.create(
|
||||
model="claude-2",
|
||||
|
|
@ -32,4 +32,3 @@ class Claude2:
|
|||
max_tokens_to_sample=1000,
|
||||
)
|
||||
return res.completion
|
||||
|
||||
|
|
@ -12,6 +12,7 @@ from dataclasses import dataclass
|
|||
@dataclass
|
||||
class BaseChatbot(ABC):
|
||||
"""Abstract GPT class"""
|
||||
|
||||
mode: str = "API"
|
||||
use_system_prompt: bool = True
|
||||
|
||||
|
|
@ -26,4 +27,3 @@ class BaseChatbot(ABC):
|
|||
@abstractmethod
|
||||
def ask_code(self, msgs: list) -> str:
|
||||
"""Ask GPT multiple questions and get a piece of code"""
|
||||
|
||||
|
|
@ -38,15 +38,19 @@ class BaseGPTAPI(BaseChatbot):
|
|||
rsp = self.completion(message)
|
||||
return self.get_choice_text(rsp)
|
||||
|
||||
async def aask(self, msg: str, system_msgs: Optional[list[str]] = None) -> str:
|
||||
async def aask(self, msg: str, system_msgs: Optional[list[str]] = None, stream=True) -> str:
|
||||
if system_msgs:
|
||||
message = self._system_msgs(system_msgs) + [self._user_msg(msg)] if self.use_system_prompt \
|
||||
message = (
|
||||
self._system_msgs(system_msgs) + [self._user_msg(msg)]
|
||||
if self.use_system_prompt
|
||||
else [self._user_msg(msg)]
|
||||
)
|
||||
else:
|
||||
message = [self._default_system_msg(), self._user_msg(msg)] if self.use_system_prompt \
|
||||
else [self._user_msg(msg)]
|
||||
rsp = await self.acompletion_text(message, stream=True)
|
||||
message = (
|
||||
[self._default_system_msg(), self._user_msg(msg)] if self.use_system_prompt else [self._user_msg(msg)]
|
||||
)
|
||||
logger.debug(message)
|
||||
rsp = await self.acompletion_text(message, stream=stream)
|
||||
# logger.debug(rsp)
|
||||
return rsp
|
||||
|
||||
|
|
|
|||
25
metagpt/provider/fireworks_api.py
Normal file
25
metagpt/provider/fireworks_api.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Desc : fireworks.ai's api
|
||||
|
||||
import openai
|
||||
|
||||
from metagpt.config import CONFIG, LLMProviderEnum
|
||||
from metagpt.provider.llm_provider_registry import register_provider
|
||||
from metagpt.provider.openai_api import CostManager, OpenAIGPTAPI, RateLimiter
|
||||
|
||||
|
||||
@register_provider(LLMProviderEnum.FIREWORKS)
|
||||
class FireWorksGPTAPI(OpenAIGPTAPI):
|
||||
def __init__(self):
|
||||
self.__init_fireworks(CONFIG)
|
||||
self.llm = openai
|
||||
self.model = CONFIG.fireworks_api_model
|
||||
self.auto_max_tokens = False
|
||||
self._cost_manager = CostManager()
|
||||
RateLimiter.__init__(self, rpm=self.rpm)
|
||||
|
||||
def __init_fireworks(self, config: "Config"):
|
||||
openai.api_key = config.fireworks_api_key
|
||||
openai.api_base = config.fireworks_api_base
|
||||
self.rpm = int(config.get("RPM", 10))
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
'''
|
||||
"""
|
||||
Filename: MetaGPT/metagpt/provider/human_provider.py
|
||||
Created Date: Wednesday, November 8th 2023, 11:55:46 pm
|
||||
Author: garylin2099
|
||||
'''
|
||||
"""
|
||||
from typing import Optional
|
||||
from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
|
||||
from metagpt.logs import logger
|
||||
from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
|
||||
|
||||
class HumanProvider(BaseGPTAPI):
|
||||
"""Humans provide themselves as a 'model', which actually takes in human input as its response.
|
||||
|
|
|
|||
34
metagpt/provider/llm_provider_registry.py
Normal file
34
metagpt/provider/llm_provider_registry.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/12/19 17:26
|
||||
@Author : alexanderwu
|
||||
@File : llm_provider_registry.py
|
||||
"""
|
||||
from metagpt.config import LLMProviderEnum
|
||||
|
||||
|
||||
class LLMProviderRegistry:
|
||||
def __init__(self):
|
||||
self.providers = {}
|
||||
|
||||
def register(self, key, provider_cls):
|
||||
self.providers[key] = provider_cls
|
||||
|
||||
def get_provider(self, enum: LLMProviderEnum):
|
||||
"""get provider instance according to the enum"""
|
||||
return self.providers[enum]()
|
||||
|
||||
|
||||
# Registry instance
|
||||
LLM_REGISTRY = LLMProviderRegistry()
|
||||
|
||||
|
||||
def register_provider(key):
|
||||
"""register provider to registry"""
|
||||
|
||||
def decorator(cls):
|
||||
LLM_REGISTRY.register(key, cls)
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
48
metagpt/provider/open_llm_api.py
Normal file
48
metagpt/provider/open_llm_api.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Desc : self-host open llm model with openai-compatible interface
|
||||
|
||||
import openai
|
||||
|
||||
from metagpt.config import CONFIG, LLMProviderEnum
|
||||
from metagpt.logs import logger
|
||||
from metagpt.provider.llm_provider_registry import register_provider
|
||||
from metagpt.provider.openai_api import CostManager, OpenAIGPTAPI, RateLimiter
|
||||
|
||||
|
||||
class OpenLLMCostManager(CostManager):
|
||||
"""open llm model is self-host, it's free and without cost"""
|
||||
|
||||
def update_cost(self, prompt_tokens, completion_tokens, model):
|
||||
"""
|
||||
Update the total cost, prompt tokens, and completion tokens.
|
||||
|
||||
Args:
|
||||
prompt_tokens (int): The number of tokens used in the prompt.
|
||||
completion_tokens (int): The number of tokens used in the completion.
|
||||
model (str): The model used for the API call.
|
||||
"""
|
||||
self.total_prompt_tokens += prompt_tokens
|
||||
self.total_completion_tokens += completion_tokens
|
||||
|
||||
logger.info(
|
||||
f"Max budget: ${CONFIG.max_budget:.3f} | "
|
||||
f"prompt_tokens: {prompt_tokens}, completion_tokens: {completion_tokens}"
|
||||
)
|
||||
CONFIG.total_cost = self.total_cost
|
||||
|
||||
|
||||
@register_provider(LLMProviderEnum.OPEN_LLM)
|
||||
class OpenLLMGPTAPI(OpenAIGPTAPI):
|
||||
def __init__(self):
|
||||
self.__init_openllm(CONFIG)
|
||||
self.llm = openai
|
||||
self.model = CONFIG.open_llm_api_model
|
||||
self.auto_max_tokens = False
|
||||
self._cost_manager = OpenLLMCostManager()
|
||||
RateLimiter.__init__(self, rpm=self.rpm)
|
||||
|
||||
def __init_openllm(self, config: "Config"):
|
||||
openai.api_key = "sk-xx" # self-host api doesn't need api-key, use the default value
|
||||
openai.api_base = config.open_llm_api_base
|
||||
self.rpm = int(config.get("RPM", 10))
|
||||
|
|
@ -25,13 +25,15 @@ from tenacity import (
|
|||
retry,
|
||||
retry_if_exception_type,
|
||||
stop_after_attempt,
|
||||
wait_fixed,
|
||||
wait_random_exponential,
|
||||
)
|
||||
|
||||
from metagpt.config import CONFIG, Config
|
||||
|
||||
from metagpt.config import CONFIG, Config, LLMProviderEnum
|
||||
from metagpt.logs import logger
|
||||
from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
from metagpt.provider.constant import GENERAL_FUNCTION_SCHEMA, GENERAL_TOOL_CHOICE
|
||||
from metagpt.provider.llm_provider_registry import register_provider
|
||||
from metagpt.schema import Message
|
||||
from metagpt.utils.singleton import Singleton
|
||||
from metagpt.utils.token_counter import (
|
||||
|
|
@ -147,6 +149,7 @@ See FAQ 5.8
|
|||
raise retry_state.outcome.exception()
|
||||
|
||||
|
||||
@register_provider(LLMProviderEnum.OPENAI)
|
||||
class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
|
||||
"""
|
||||
Check https://platform.openai.com/examples for examples
|
||||
|
|
@ -259,8 +262,8 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
|
|||
return await self._achat_completion(messages)
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_fixed(1),
|
||||
wait=wait_random_exponential(min=1, max=60),
|
||||
stop=stop_after_attempt(6),
|
||||
after=after_log(logger, logger.level("WARNING").name),
|
||||
retry=retry_if_exception_type(APIConnectionError),
|
||||
retry_error_callback=log_and_reraise,
|
||||
|
|
@ -366,16 +369,17 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
|
|||
|
||||
def _calc_usage(self, messages: list[dict], rsp: str) -> CompletionUsage:
|
||||
usage = CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0)
|
||||
if CONFIG.calc_usage:
|
||||
try:
|
||||
usage.prompt_tokens = count_message_tokens(messages, self.model)
|
||||
usage.completion_tokens = count_string_tokens(rsp, self.model)
|
||||
return usage
|
||||
except Exception as e:
|
||||
logger.error(f"usage calculation failed!: {e}")
|
||||
else:
|
||||
if not CONFIG.calc_usage:
|
||||
return usage
|
||||
|
||||
try:
|
||||
usage.prompt_tokens = count_message_tokens(messages, self.model)
|
||||
usage.completion_tokens = count_string_tokens(rsp, self.model)
|
||||
except Exception as e:
|
||||
logger.error(f"usage calculation failed!: {e}")
|
||||
|
||||
return usage
|
||||
|
||||
async def acompletion_batch(self, batch: list[list[dict]]) -> list[ChatCompletion]:
|
||||
"""Return full JSON"""
|
||||
split_batches = self.split_batches(batch)
|
||||
|
|
@ -403,7 +407,7 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
|
|||
return results
|
||||
|
||||
def _update_costs(self, usage: CompletionUsage):
|
||||
if CONFIG.calc_usage:
|
||||
if CONFIG.calc_usage and usage:
|
||||
try:
|
||||
self._cost_manager.update_cost(usage.prompt_tokens, usage.completion_tokens, self.model)
|
||||
except Exception as e:
|
||||
|
|
|
|||
3
metagpt/provider/postprecess/__init__.py
Normal file
3
metagpt/provider/postprecess/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Desc :
|
||||
69
metagpt/provider/postprecess/base_postprecess_plugin.py
Normal file
69
metagpt/provider/postprecess/base_postprecess_plugin.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Desc : base llm postprocess plugin to do the operations like repair the raw llm output
|
||||
|
||||
from typing import Union
|
||||
|
||||
from metagpt.utils.repair_llm_raw_output import (
|
||||
RepairType,
|
||||
extract_content_from_output,
|
||||
repair_llm_raw_output,
|
||||
retry_parse_json_text,
|
||||
)
|
||||
|
||||
|
||||
class BasePostPrecessPlugin(object):
|
||||
model = None # the plugin of the `model`, use to judge in `llm_postprecess`
|
||||
|
||||
def run_repair_llm_output(self, output: str, schema: dict, req_key: str = "[/CONTENT]") -> Union[dict, list]:
|
||||
"""
|
||||
repair steps
|
||||
1. repair the case sensitive problem using the schema's fields
|
||||
2. extract the content from the req_key pair( xx[REQ_KEY]xxx[/REQ_KEY]xx )
|
||||
3. repair the invalid json text in the content
|
||||
4. parse the json text and repair it according to the exception with retry loop
|
||||
"""
|
||||
output_class_fields = list(schema["properties"].keys()) # Custom ActionOutput's fields
|
||||
|
||||
content = self.run_repair_llm_raw_output(output, req_keys=output_class_fields + [req_key])
|
||||
content = self.run_extract_content_from_output(content, right_key=req_key)
|
||||
# # req_keys mocked
|
||||
content = self.run_repair_llm_raw_output(content, req_keys=[None], repair_type=RepairType.JSON)
|
||||
parsed_data = self.run_retry_parse_json_text(content)
|
||||
|
||||
return parsed_data
|
||||
|
||||
def run_repair_llm_raw_output(self, content: str, req_keys: list[str], repair_type: str = None) -> str:
|
||||
"""inherited class can re-implement the function"""
|
||||
return repair_llm_raw_output(content, req_keys=req_keys, repair_type=repair_type)
|
||||
|
||||
def run_extract_content_from_output(self, content: str, right_key: str) -> str:
|
||||
"""inherited class can re-implement the function"""
|
||||
return extract_content_from_output(content, right_key=right_key)
|
||||
|
||||
def run_retry_parse_json_text(self, content: str) -> Union[dict, list]:
|
||||
"""inherited class can re-implement the function"""
|
||||
# logger.info(f"extracted json CONTENT from output:\n{content}")
|
||||
parsed_data = retry_parse_json_text(output=content) # should use output=content
|
||||
return parsed_data
|
||||
|
||||
def run(self, output: str, schema: dict, req_key: str = "[/CONTENT]") -> Union[dict, list]:
|
||||
"""
|
||||
this is used for prompt with a json-format output requirement and outer pair key, like
|
||||
[REQ_KEY]
|
||||
{
|
||||
"Key": "value"
|
||||
}
|
||||
[/REQ_KEY]
|
||||
|
||||
Args
|
||||
outer (str): llm raw output
|
||||
schema: output json schema
|
||||
req_key: outer pair right key, usually in `[/REQ_KEY]` format
|
||||
"""
|
||||
assert len(schema.get("properties")) > 0
|
||||
assert "/" in req_key
|
||||
|
||||
# current, postprocess only deal the repair_llm_raw_output
|
||||
new_output = self.run_repair_llm_output(output=output, schema=schema, req_key=req_key)
|
||||
return new_output
|
||||
20
metagpt/provider/postprecess/llm_output_postprecess.py
Normal file
20
metagpt/provider/postprecess/llm_output_postprecess.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Desc : the entry of choosing which PostProcessPlugin to deal particular LLM model's output
|
||||
|
||||
from typing import Union
|
||||
|
||||
from metagpt.provider.postprecess.base_postprecess_plugin import BasePostPrecessPlugin
|
||||
|
||||
|
||||
def llm_output_postprecess(
|
||||
output: str, schema: dict, req_key: str = "[/CONTENT]", model_name: str = None
|
||||
) -> Union[dict, str]:
|
||||
"""
|
||||
default use BasePostPrecessPlugin if there is not matched plugin.
|
||||
"""
|
||||
# TODO choose different model's plugin according to the model_name
|
||||
postprecess_plugin = BasePostPrecessPlugin()
|
||||
|
||||
result = postprecess_plugin.run(output=output, schema=schema, req_key=req_key)
|
||||
return result
|
||||
|
|
@ -14,21 +14,21 @@ import json
|
|||
import ssl
|
||||
from time import mktime
|
||||
from typing import Optional
|
||||
from urllib.parse import urlencode
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlencode, urlparse
|
||||
from wsgiref.handlers import format_date_time
|
||||
|
||||
import websocket # 使用websocket_client
|
||||
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.config import CONFIG, LLMProviderEnum
|
||||
from metagpt.logs import logger
|
||||
from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
from metagpt.provider.llm_provider_registry import register_provider
|
||||
|
||||
|
||||
@register_provider(LLMProviderEnum.SPARK)
|
||||
class SparkAPI(BaseGPTAPI):
|
||||
|
||||
def __init__(self):
|
||||
logger.warning('当前方法无法支持异步运行。当你使用acompletion时,并不能并行访问。')
|
||||
logger.warning("当前方法无法支持异步运行。当你使用acompletion时,并不能并行访问。")
|
||||
|
||||
def ask(self, msg: str) -> str:
|
||||
message = [self._default_system_msg(), self._user_msg(msg)]
|
||||
|
|
@ -49,7 +49,7 @@ class SparkAPI(BaseGPTAPI):
|
|||
|
||||
async def acompletion_text(self, messages: list[dict], stream=False) -> str:
|
||||
# 不支持
|
||||
logger.error('该功能禁用。')
|
||||
logger.error("该功能禁用。")
|
||||
w = GetMessageFromWeb(messages)
|
||||
return w.run()
|
||||
|
||||
|
|
@ -93,29 +93,26 @@ class GetMessageFromWeb:
|
|||
signature_origin += "GET " + self.path + " HTTP/1.1"
|
||||
|
||||
# 进行hmac-sha256进行加密
|
||||
signature_sha = hmac.new(self.api_secret.encode('utf-8'), signature_origin.encode('utf-8'),
|
||||
digestmod=hashlib.sha256).digest()
|
||||
signature_sha = hmac.new(
|
||||
self.api_secret.encode("utf-8"), signature_origin.encode("utf-8"), digestmod=hashlib.sha256
|
||||
).digest()
|
||||
|
||||
signature_sha_base64 = base64.b64encode(signature_sha).decode(encoding='utf-8')
|
||||
signature_sha_base64 = base64.b64encode(signature_sha).decode(encoding="utf-8")
|
||||
|
||||
authorization_origin = f'api_key="{self.api_key}", algorithm="hmac-sha256", headers="host date request-line", signature="{signature_sha_base64}"'
|
||||
|
||||
authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8')
|
||||
authorization = base64.b64encode(authorization_origin.encode("utf-8")).decode(encoding="utf-8")
|
||||
|
||||
# 将请求的鉴权参数组合为字典
|
||||
v = {
|
||||
"authorization": authorization,
|
||||
"date": date,
|
||||
"host": self.host
|
||||
}
|
||||
v = {"authorization": authorization, "date": date, "host": self.host}
|
||||
# 拼接鉴权参数,生成url
|
||||
url = self.spark_url + '?' + urlencode(v)
|
||||
url = self.spark_url + "?" + urlencode(v)
|
||||
# 此处打印出建立连接时候的url,参考本demo的时候可取消上方打印的注释,比对相同参数时生成的url与自己代码生成的url是否一致
|
||||
return url
|
||||
|
||||
def __init__(self, text):
|
||||
self.text = text
|
||||
self.ret = ''
|
||||
self.ret = ""
|
||||
self.spark_appid = CONFIG.spark_appid
|
||||
self.spark_api_secret = CONFIG.spark_api_secret
|
||||
self.spark_api_key = CONFIG.spark_api_key
|
||||
|
|
@ -124,15 +121,15 @@ class GetMessageFromWeb:
|
|||
|
||||
def on_message(self, ws, message):
|
||||
data = json.loads(message)
|
||||
code = data['header']['code']
|
||||
code = data["header"]["code"]
|
||||
|
||||
if code != 0:
|
||||
ws.close() # 请求错误,则关闭socket
|
||||
logger.critical(f'回答获取失败,响应信息反序列化之后为: {data}')
|
||||
logger.critical(f"回答获取失败,响应信息反序列化之后为: {data}")
|
||||
return
|
||||
else:
|
||||
choices = data["payload"]["choices"]
|
||||
seq = choices["seq"] # 服务端是流式返回,seq为返回的数据序号
|
||||
# seq = choices["seq"] # 服务端是流式返回,seq为返回的数据序号
|
||||
status = choices["status"] # 服务端是流式返回,status用于判断信息是否传送完毕
|
||||
content = choices["text"][0]["content"] # 本次接收到的回答文本
|
||||
self.ret += content
|
||||
|
|
@ -142,7 +139,7 @@ class GetMessageFromWeb:
|
|||
# 收到websocket错误的处理
|
||||
def on_error(self, ws, error):
|
||||
# on_message方法处理接收到的信息,出现任何错误,都会调用这个方法
|
||||
logger.critical(f'通讯连接出错,【错误提示: {error}】')
|
||||
logger.critical(f"通讯连接出错,【错误提示: {error}】")
|
||||
|
||||
# 收到websocket关闭的处理
|
||||
def on_close(self, ws, one, two):
|
||||
|
|
@ -150,17 +147,12 @@ class GetMessageFromWeb:
|
|||
|
||||
# 处理请求数据
|
||||
def gen_params(self):
|
||||
|
||||
data = {
|
||||
"header": {
|
||||
"app_id": self.spark_appid,
|
||||
"uid": "1234"
|
||||
},
|
||||
"header": {"app_id": self.spark_appid, "uid": "1234"},
|
||||
"parameter": {
|
||||
"chat": {
|
||||
# domain为必传参数
|
||||
"domain": self.domain,
|
||||
|
||||
# 以下为可微调,非必传参数
|
||||
# 注意:官方建议,temperature和top_k修改一个即可
|
||||
"max_tokens": 2048, # 默认2048,模型回答的tokens的最大长度,即允许它输出文本的最长字数
|
||||
|
|
@ -168,11 +160,7 @@ class GetMessageFromWeb:
|
|||
"top_k": 4, # 取值为[1,6],默认为4。从k个候选中随机选择一个(非等概率)
|
||||
}
|
||||
},
|
||||
"payload": {
|
||||
"message": {
|
||||
"text": self.text
|
||||
}
|
||||
}
|
||||
"payload": {"message": {"text": self.text}},
|
||||
}
|
||||
return data
|
||||
|
||||
|
|
@ -189,17 +177,12 @@ class GetMessageFromWeb:
|
|||
return self._run(self.text)
|
||||
|
||||
def _run(self, text_list):
|
||||
|
||||
ws_param = self.WsParam(
|
||||
self.spark_appid,
|
||||
self.spark_api_key,
|
||||
self.spark_api_secret,
|
||||
self.spark_url,
|
||||
text_list)
|
||||
ws_param = self.WsParam(self.spark_appid, self.spark_api_key, self.spark_api_secret, self.spark_url, text_list)
|
||||
ws_url = ws_param.create_url()
|
||||
|
||||
websocket.enableTrace(False) # 默认禁用 WebSocket 的跟踪功能
|
||||
ws = websocket.WebSocketApp(ws_url, on_message=self.on_message, on_error=self.on_error, on_close=self.on_close,
|
||||
on_open=self.on_open)
|
||||
ws = websocket.WebSocketApp(
|
||||
ws_url, on_message=self.on_message, on_error=self.on_error, on_close=self.on_close, on_open=self.on_open
|
||||
)
|
||||
ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
|
||||
return self.ret
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@
|
|||
# @Desc : async_sse_client to make keep the use of Event to access response
|
||||
# refs to `https://github.com/zhipuai/zhipuai-sdk-python/blob/main/zhipuai/utils/sse_client.py`
|
||||
|
||||
from zhipuai.utils.sse_client import SSEClient, Event, _FIELD_SEPARATOR
|
||||
from zhipuai.utils.sse_client import _FIELD_SEPARATOR, Event, SSEClient
|
||||
|
||||
|
||||
class AsyncSSEClient(SSEClient):
|
||||
|
||||
async def _aread(self):
|
||||
data = b""
|
||||
async for chunk in self._event_source:
|
||||
|
|
@ -37,9 +36,7 @@ class AsyncSSEClient(SSEClient):
|
|||
|
||||
# Ignore unknown fields.
|
||||
if field not in event.__dict__:
|
||||
self._logger.debug(
|
||||
"Saw invalid field %s while parsing " "Server Side Event", field
|
||||
)
|
||||
self._logger.debug("Saw invalid field %s while parsing " "Server Side Event", field)
|
||||
continue
|
||||
|
||||
if len(data) > 1:
|
||||
|
|
|
|||
|
|
@ -2,23 +2,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# @Desc : zhipuai LLM from https://open.bigmodel.cn/dev/api#sdk
|
||||
|
||||
from enum import Enum
|
||||
import json
|
||||
from enum import Enum
|
||||
|
||||
import openai
|
||||
import zhipuai
|
||||
from requests import ConnectionError
|
||||
from tenacity import (
|
||||
after_log,
|
||||
retry,
|
||||
retry_if_exception_type,
|
||||
stop_after_attempt,
|
||||
wait_fixed,
|
||||
wait_random_exponential,
|
||||
)
|
||||
from requests import ConnectionError
|
||||
|
||||
import openai
|
||||
import zhipuai
|
||||
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.config import CONFIG, LLMProviderEnum
|
||||
from metagpt.logs import logger
|
||||
from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
from metagpt.provider.llm_provider_registry import register_provider
|
||||
from metagpt.provider.openai_api import CostManager, log_and_reraise
|
||||
from metagpt.provider.zhipuai.zhipu_model_api import ZhiPuModelAPI
|
||||
|
||||
|
|
@ -30,6 +31,7 @@ class ZhiPuEvent(Enum):
|
|||
FINISH = "finish"
|
||||
|
||||
|
||||
@register_provider(LLMProviderEnum.ZHIPUAI)
|
||||
class ZhiPuAIGPTAPI(BaseGPTAPI):
|
||||
"""
|
||||
Refs to `https://open.bigmodel.cn/dev/api#chatglm_turbo`
|
||||
|
|
@ -50,15 +52,11 @@ class ZhiPuAIGPTAPI(BaseGPTAPI):
|
|||
openai.api_key = zhipuai.api_key # due to use openai sdk, set the api_key but it will't be used.
|
||||
|
||||
def _const_kwargs(self, messages: list[dict]) -> dict:
|
||||
kwargs = {
|
||||
"model": self.model,
|
||||
"prompt": messages,
|
||||
"temperature": 0.3
|
||||
}
|
||||
kwargs = {"model": self.model, "prompt": messages, "temperature": 0.3}
|
||||
return kwargs
|
||||
|
||||
def _update_costs(self, usage: dict):
|
||||
""" update each request's token cost """
|
||||
"""update each request's token cost"""
|
||||
if CONFIG.calc_usage:
|
||||
try:
|
||||
prompt_tokens = int(usage.get("prompt_tokens", 0))
|
||||
|
|
@ -68,7 +66,7 @@ class ZhiPuAIGPTAPI(BaseGPTAPI):
|
|||
logger.error("zhipuai updats costs failed!", e)
|
||||
|
||||
def get_choice_text(self, resp: dict) -> str:
|
||||
""" get the first text of choice from llm response """
|
||||
"""get the first text of choice from llm response"""
|
||||
assist_msg = resp.get("data", {}).get("choices", [{"role": "error"}])[-1]
|
||||
assert assist_msg["role"] == "assistant"
|
||||
return assist_msg.get("content")
|
||||
|
|
@ -126,13 +124,13 @@ class ZhiPuAIGPTAPI(BaseGPTAPI):
|
|||
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_fixed(1),
|
||||
wait=wait_random_exponential(min=1, max=60),
|
||||
after=after_log(logger, logger.level("WARNING").name),
|
||||
retry=retry_if_exception_type(ConnectionError),
|
||||
retry_error_callback=log_and_reraise
|
||||
retry_error_callback=log_and_reraise,
|
||||
)
|
||||
async def acompletion_text(self, messages: list[dict], stream=False) -> str:
|
||||
""" response in async with stream or non-stream mode """
|
||||
"""response in async with stream or non-stream mode"""
|
||||
if stream:
|
||||
return await self._achat_completion_stream(messages)
|
||||
resp = await self._achat_completion(messages)
|
||||
|
|
|
|||
99
metagpt/repo_parser.py
Normal file
99
metagpt/repo_parser.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/11/17 17:58
|
||||
@Author : alexanderwu
|
||||
@File : repo_parser.py
|
||||
"""
|
||||
import ast
|
||||
import json
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
|
||||
import pandas as pd
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.logs import logger
|
||||
from metagpt.utils.exceptions import handle_exception
|
||||
|
||||
|
||||
class RepoParser(BaseModel):
|
||||
base_directory: Path = Field(default=None)
|
||||
|
||||
@classmethod
|
||||
@handle_exception(exception_type=Exception, default_return=[])
|
||||
def _parse_file(cls, file_path: Path) -> list:
|
||||
"""Parse a Python file in the repository."""
|
||||
return ast.parse(file_path.read_text()).body
|
||||
|
||||
def extract_class_and_function_info(self, tree, file_path):
|
||||
"""Extract class, function, and global variable information from the AST."""
|
||||
file_info = {
|
||||
"file": str(file_path.relative_to(self.base_directory)),
|
||||
"classes": [],
|
||||
"functions": [],
|
||||
"globals": [],
|
||||
}
|
||||
|
||||
for node in tree:
|
||||
if isinstance(node, ast.ClassDef):
|
||||
class_methods = [m.name for m in node.body if is_func(m)]
|
||||
file_info["classes"].append({"name": node.name, "methods": class_methods})
|
||||
elif is_func(node):
|
||||
file_info["functions"].append(node.name)
|
||||
elif isinstance(node, (ast.Assign, ast.AnnAssign)):
|
||||
for target in node.targets if isinstance(node, ast.Assign) else [node.target]:
|
||||
if isinstance(target, ast.Name):
|
||||
file_info["globals"].append(target.id)
|
||||
return file_info
|
||||
|
||||
def generate_symbols(self):
|
||||
files_classes = []
|
||||
directory = self.base_directory
|
||||
for path in directory.rglob("*.py"):
|
||||
tree = self._parse_file(path)
|
||||
file_info = self.extract_class_and_function_info(tree, path)
|
||||
files_classes.append(file_info)
|
||||
|
||||
return files_classes
|
||||
|
||||
def generate_json_structure(self, output_path):
|
||||
"""Generate a JSON file documenting the repository structure."""
|
||||
files_classes = self.generate_symbols()
|
||||
output_path.write_text(json.dumps(files_classes, indent=4))
|
||||
|
||||
def generate_dataframe_structure(self, output_path):
|
||||
"""Generate a DataFrame documenting the repository structure and save as CSV."""
|
||||
files_classes = self.generate_symbols()
|
||||
df = pd.DataFrame(files_classes)
|
||||
df.to_csv(output_path, index=False)
|
||||
|
||||
def generate_structure(self, output_path=None, mode="json"):
|
||||
"""Generate the structure of the repository as a specified format."""
|
||||
output_file = self.base_directory / f"{self.base_directory.name}-structure.{mode}"
|
||||
output_path = Path(output_path) if output_path else output_file
|
||||
|
||||
if mode == "json":
|
||||
self.generate_json_structure(output_path)
|
||||
elif mode == "csv":
|
||||
self.generate_dataframe_structure(output_path)
|
||||
|
||||
|
||||
def is_func(node):
|
||||
return isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||
|
||||
|
||||
def main():
|
||||
repo_parser = RepoParser(base_directory=CONFIG.workspace_path / "web_2048")
|
||||
symbols = repo_parser.generate_symbols()
|
||||
logger.info(pformat(symbols))
|
||||
|
||||
|
||||
def error():
|
||||
"""raise Exception and logs it"""
|
||||
RepoParser._parse_file(Path("test.py"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -12,7 +12,7 @@ from metagpt.roles.project_manager import ProjectManager
|
|||
from metagpt.roles.product_manager import ProductManager
|
||||
from metagpt.roles.engineer import Engineer
|
||||
from metagpt.roles.qa_engineer import QaEngineer
|
||||
from metagpt.roles.seacher import Searcher
|
||||
from metagpt.roles.searcher import Searcher
|
||||
from metagpt.roles.sales import Sales
|
||||
from metagpt.roles.customer_service import CustomerService
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
from metagpt.actions import WritePRD
|
||||
from metagpt.actions.design_api import WriteDesign
|
||||
from metagpt.roles import Role
|
||||
from metagpt.roles.role import Role
|
||||
|
||||
|
||||
class Architect(Role):
|
||||
|
|
@ -22,16 +22,16 @@ class Architect(Role):
|
|||
constraints (str): Constraints or guidelines for the architect.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "Bob",
|
||||
profile: str = "Architect",
|
||||
goal: str = "Design a concise, usable, complete python system",
|
||||
constraints: str = "Try to specify good open source tools as much as possible",
|
||||
) -> None:
|
||||
"""Initializes the Architect with given attributes."""
|
||||
super().__init__(name, profile, goal, constraints)
|
||||
name: str = "Bob"
|
||||
profile: str = "Architect"
|
||||
goal: str = "design a concise, usable, complete software system"
|
||||
constraints: str = (
|
||||
"make sure the architecture is simple enough and use appropriate open source "
|
||||
"libraries. Use same language as user requirement"
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
# Initialize actions specific to the Architect role
|
||||
self._init_actions([WriteDesign])
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
@Author : alexanderwu
|
||||
@File : sales.py
|
||||
"""
|
||||
from typing import Optional
|
||||
|
||||
from metagpt.roles import Sales
|
||||
|
||||
# from metagpt.actions import SearchAndSummarize
|
||||
|
|
@ -24,12 +26,11 @@ DESC = """
|
|||
|
||||
|
||||
class CustomerService(Sales):
|
||||
def __init__(
|
||||
self,
|
||||
name="Xiaomei",
|
||||
profile="Human customer service",
|
||||
desc=DESC,
|
||||
store=None
|
||||
):
|
||||
super().__init__(name, profile, desc=desc, store=store)
|
||||
|
||||
name: str = "Xiaomei"
|
||||
profile: str = "Human customer service"
|
||||
desc: str = DESC
|
||||
|
||||
store: Optional[str] = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
|
|
|||
|
|
@ -4,46 +4,55 @@
|
|||
@Time : 2023/5/11 14:43
|
||||
@Author : alexanderwu
|
||||
@File : engineer.py
|
||||
@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116:
|
||||
1. Modify the data type of the `cause_by` value in the `Message` to a string, and utilize the new message
|
||||
distribution feature for message filtering.
|
||||
2. Consolidate message reception and processing logic within `_observe`.
|
||||
3. Fix bug: Add logic for handling asynchronous message processing when messages are not ready.
|
||||
4. Supplemented the external transmission of internal messages.
|
||||
@Modified By: mashenquan, 2023-11-27.
|
||||
1. According to Section 2.2.3.1 of RFC 135, replace file data in the message with the file name.
|
||||
2. According to the design in Section 2.2.3.5.5 of RFC 135, add incremental iteration functionality.
|
||||
@Modified By: mashenquan, 2023-12-5. Enhance the workflow to navigate to WriteCode or QaEngineer based on the results
|
||||
of SummarizeCode.
|
||||
"""
|
||||
import asyncio
|
||||
import shutil
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
|
||||
from metagpt.actions import WriteCode, WriteCodeReview, WriteDesign, WriteTasks
|
||||
from metagpt.const import WORKSPACE_ROOT
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Set
|
||||
|
||||
from metagpt.actions import Action, WriteCode, WriteCodeReview, WriteTasks
|
||||
from metagpt.actions.fix_bug import FixBug
|
||||
from metagpt.actions.summarize_code import SummarizeCode
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.const import (
|
||||
CODE_SUMMARIES_FILE_REPO,
|
||||
CODE_SUMMARIES_PDF_FILE_REPO,
|
||||
SYSTEM_DESIGN_FILE_REPO,
|
||||
TASK_FILE_REPO,
|
||||
)
|
||||
from metagpt.logs import logger
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import Message
|
||||
from metagpt.utils.common import CodeParser
|
||||
from metagpt.utils.special_tokens import FILENAME_CODE_SEP, MSG_SEP
|
||||
from metagpt.schema import (
|
||||
CodeSummarizeContext,
|
||||
CodingContext,
|
||||
Document,
|
||||
Documents,
|
||||
Message,
|
||||
)
|
||||
from metagpt.utils.common import any_to_str, any_to_str_set
|
||||
|
||||
IS_PASS_PROMPT = """
|
||||
{context}
|
||||
|
||||
async def gather_ordered_k(coros, k) -> list:
|
||||
tasks = OrderedDict()
|
||||
results = [None] * len(coros)
|
||||
done_queue = asyncio.Queue()
|
||||
|
||||
for i, coro in enumerate(coros):
|
||||
if len(tasks) >= k:
|
||||
done, _ = await asyncio.wait(tasks.keys(), return_when=asyncio.FIRST_COMPLETED)
|
||||
for task in done:
|
||||
index = tasks.pop(task)
|
||||
await done_queue.put((index, task.result()))
|
||||
task = asyncio.create_task(coro)
|
||||
tasks[task] = i
|
||||
|
||||
if tasks:
|
||||
done, _ = await asyncio.wait(tasks.keys())
|
||||
for task in done:
|
||||
index = tasks[task]
|
||||
await done_queue.put((index, task.result()))
|
||||
|
||||
while not done_queue.empty():
|
||||
index, result = await done_queue.get()
|
||||
results[index] = result
|
||||
|
||||
return results
|
||||
----
|
||||
Does the above log indicate anything that needs to be done?
|
||||
If there are any tasks to be completed, please answer 'NO' along with the to-do list in JSON format;
|
||||
otherwise, answer 'YES' in JSON format.
|
||||
"""
|
||||
|
||||
|
||||
class Engineer(Role):
|
||||
|
|
@ -57,119 +66,35 @@ class Engineer(Role):
|
|||
constraints (str): Constraints for the engineer.
|
||||
n_borg (int): Number of borgs.
|
||||
use_code_review (bool): Whether to use code review.
|
||||
todos (list): List of tasks.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "Alex",
|
||||
profile: str = "Engineer",
|
||||
goal: str = "Write elegant, readable, extensible, efficient code",
|
||||
constraints: str = "The code should conform to standards like PEP8 and be modular and maintainable",
|
||||
n_borg: int = 1,
|
||||
use_code_review: bool = False,
|
||||
) -> None:
|
||||
"""Initializes the Engineer role with given attributes."""
|
||||
super().__init__(name, profile, goal, constraints)
|
||||
name: str = "Alex"
|
||||
profile: str = "Engineer"
|
||||
goal: str = "write elegant, readable, extensible, efficient code"
|
||||
constraints: str = (
|
||||
"the code should conform to standards like google-style and be modular and maintainable. "
|
||||
"Use same language as user requirement"
|
||||
)
|
||||
n_borg: int = 1
|
||||
use_code_review: bool = False
|
||||
code_todos: list = []
|
||||
summarize_todos = []
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._init_actions([WriteCode])
|
||||
self.use_code_review = use_code_review
|
||||
if self.use_code_review:
|
||||
self._init_actions([WriteCode, WriteCodeReview])
|
||||
self._watch([WriteTasks])
|
||||
self.todos = []
|
||||
self.n_borg = n_borg
|
||||
self._watch([WriteTasks, SummarizeCode, WriteCode, WriteCodeReview, FixBug])
|
||||
|
||||
@classmethod
|
||||
def parse_tasks(self, task_msg: Message) -> list[str]:
|
||||
if task_msg.instruct_content:
|
||||
return task_msg.instruct_content.dict().get("Task list")
|
||||
return CodeParser.parse_file_list(block="Task list", text=task_msg.content)
|
||||
@staticmethod
|
||||
def _parse_tasks(task_msg: Document) -> list[str]:
|
||||
m = json.loads(task_msg.content)
|
||||
return m.get("Task list")
|
||||
|
||||
@classmethod
|
||||
def parse_code(self, code_text: str) -> str:
|
||||
return CodeParser.parse_code(block="", text=code_text)
|
||||
|
||||
@classmethod
|
||||
def parse_workspace(cls, system_design_msg: Message) -> str:
|
||||
if system_design_msg.instruct_content:
|
||||
return system_design_msg.instruct_content.dict().get("Python package name").strip().strip("'").strip('"')
|
||||
return CodeParser.parse_str(block="Python package name", text=system_design_msg.content)
|
||||
|
||||
def get_workspace(self) -> Path:
|
||||
msg = self._rc.memory.get_by_action(WriteDesign)[-1]
|
||||
if not msg:
|
||||
return WORKSPACE_ROOT / "src"
|
||||
workspace = self.parse_workspace(msg)
|
||||
# Codes are written in workspace/{package_name}/{package_name}
|
||||
return WORKSPACE_ROOT / workspace / workspace
|
||||
|
||||
def recreate_workspace(self):
|
||||
workspace = self.get_workspace()
|
||||
try:
|
||||
shutil.rmtree(workspace)
|
||||
except FileNotFoundError:
|
||||
pass # The folder does not exist, but we don't care
|
||||
workspace.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def write_file(self, filename: str, code: str):
|
||||
workspace = self.get_workspace()
|
||||
filename = filename.replace('"', "").replace("\n", "")
|
||||
file = workspace / filename
|
||||
file.parent.mkdir(parents=True, exist_ok=True)
|
||||
file.write_text(code)
|
||||
return file
|
||||
|
||||
def recv(self, message: Message) -> None:
|
||||
self._rc.memory.add(message)
|
||||
if message in self._rc.important_memory:
|
||||
self.todos = self.parse_tasks(message)
|
||||
|
||||
async def _act_mp(self) -> Message:
|
||||
# self.recreate_workspace()
|
||||
todo_coros = []
|
||||
for todo in self.todos:
|
||||
todo_coro = WriteCode().run(
|
||||
context=self._rc.memory.get_by_actions([WriteTasks, WriteDesign]), filename=todo
|
||||
)
|
||||
todo_coros.append(todo_coro)
|
||||
|
||||
rsps = await gather_ordered_k(todo_coros, self.n_borg)
|
||||
for todo, code_rsp in zip(self.todos, rsps):
|
||||
_ = self.parse_code(code_rsp)
|
||||
logger.info(todo)
|
||||
logger.info(code_rsp)
|
||||
# self.write_file(todo, code)
|
||||
msg = Message(content=code_rsp, role=self.profile, cause_by=type(self._rc.todo))
|
||||
self._rc.memory.add(msg)
|
||||
del self.todos[0]
|
||||
|
||||
logger.info(f"Done {self.get_workspace()} generating.")
|
||||
msg = Message(content="all done.", role=self.profile, cause_by=type(self._rc.todo))
|
||||
return msg
|
||||
|
||||
async def _act_sp(self) -> Message:
|
||||
code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later
|
||||
for todo in self.todos:
|
||||
code = await WriteCode().run(context=self._rc.history, filename=todo)
|
||||
# logger.info(todo)
|
||||
# logger.info(code_rsp)
|
||||
# code = self.parse_code(code_rsp)
|
||||
file_path = self.write_file(todo, code)
|
||||
msg = Message(content=code, role=self.profile, cause_by=type(self._rc.todo))
|
||||
self._rc.memory.add(msg)
|
||||
|
||||
code_msg = todo + FILENAME_CODE_SEP + str(file_path)
|
||||
code_msg_all.append(code_msg)
|
||||
|
||||
logger.info(f"Done {self.get_workspace()} generating.")
|
||||
msg = Message(
|
||||
content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=type(self._rc.todo), send_to="QaEngineer"
|
||||
)
|
||||
return msg
|
||||
|
||||
async def _act_sp_precision(self) -> Message:
|
||||
code_msg_all = [] # gather all code info, will pass to qa_engineer for tests later
|
||||
for todo in self.todos:
|
||||
async def _act_sp_with_cr(self, review=False) -> Set[str]:
|
||||
changed_files = set()
|
||||
src_file_repo = CONFIG.git_repo.new_file_repository(CONFIG.src_workspace)
|
||||
for todo in self.code_todos:
|
||||
"""
|
||||
# Select essential information from the historical data to reduce the length of the prompt (summarized from human experience):
|
||||
1. All from Architect
|
||||
|
|
@ -177,37 +102,202 @@ class Engineer(Role):
|
|||
3. Do we need other codes (currently needed)?
|
||||
TODO: The goal is not to need it. After clear task decomposition, based on the design idea, you should be able to write a single file without needing other codes. If you can't, it means you need a clearer definition. This is the key to writing longer code.
|
||||
"""
|
||||
context = []
|
||||
msg = self._rc.memory.get_by_actions([WriteDesign, WriteTasks, WriteCode])
|
||||
for m in msg:
|
||||
context.append(m.content)
|
||||
context_str = "\n".join(context)
|
||||
# Write code
|
||||
code = await WriteCode().run(context=context_str, filename=todo)
|
||||
coding_context = await todo.run()
|
||||
# Code review
|
||||
if self.use_code_review:
|
||||
try:
|
||||
rewrite_code = await WriteCodeReview().run(context=context_str, code=code, filename=todo)
|
||||
code = rewrite_code
|
||||
except Exception as e:
|
||||
logger.error("code review failed!", e)
|
||||
pass
|
||||
file_path = self.write_file(todo, code)
|
||||
msg = Message(content=code, role=self.profile, cause_by=WriteCode)
|
||||
if review:
|
||||
action = WriteCodeReview(context=coding_context, llm=self._llm)
|
||||
self._init_action_system_message(action)
|
||||
coding_context = await action.run()
|
||||
await src_file_repo.save(
|
||||
coding_context.filename,
|
||||
dependencies={coding_context.design_doc.root_relative_path, coding_context.task_doc.root_relative_path},
|
||||
content=coding_context.code_doc.content,
|
||||
)
|
||||
msg = Message(
|
||||
content=coding_context.json(), instruct_content=coding_context, role=self.profile, cause_by=WriteCode
|
||||
)
|
||||
self._rc.memory.add(msg)
|
||||
|
||||
code_msg = todo + FILENAME_CODE_SEP + str(file_path)
|
||||
code_msg_all.append(code_msg)
|
||||
changed_files.add(coding_context.code_doc.filename)
|
||||
if not changed_files:
|
||||
logger.info("Nothing has changed.")
|
||||
return changed_files
|
||||
|
||||
logger.info(f"Done {self.get_workspace()} generating.")
|
||||
msg = Message(
|
||||
content=MSG_SEP.join(code_msg_all), role=self.profile, cause_by=type(self._rc.todo), send_to="QaEngineer"
|
||||
)
|
||||
return msg
|
||||
|
||||
async def _act(self) -> Message:
|
||||
async def _act(self) -> Message | None:
|
||||
"""Determines the mode of action based on whether code review is used."""
|
||||
logger.info(f"{self._setting}: ready to WriteCode")
|
||||
if self.use_code_review:
|
||||
return await self._act_sp_precision()
|
||||
return await self._act_sp()
|
||||
if self._rc.todo is None:
|
||||
return None
|
||||
if isinstance(self._rc.todo, WriteCode):
|
||||
return await self._act_write_code()
|
||||
if isinstance(self._rc.todo, SummarizeCode):
|
||||
return await self._act_summarize()
|
||||
return None
|
||||
|
||||
async def _act_write_code(self):
|
||||
changed_files = await self._act_sp_with_cr(review=self.use_code_review)
|
||||
return Message(
|
||||
content="\n".join(changed_files),
|
||||
role=self.profile,
|
||||
cause_by=WriteCodeReview if self.use_code_review else WriteCode,
|
||||
send_to=self,
|
||||
sent_from=self,
|
||||
)
|
||||
|
||||
async def _act_summarize(self):
|
||||
code_summaries_file_repo = CONFIG.git_repo.new_file_repository(CODE_SUMMARIES_FILE_REPO)
|
||||
code_summaries_pdf_file_repo = CONFIG.git_repo.new_file_repository(CODE_SUMMARIES_PDF_FILE_REPO)
|
||||
tasks = []
|
||||
src_relative_path = CONFIG.src_workspace.relative_to(CONFIG.git_repo.workdir)
|
||||
for todo in self.summarize_todos:
|
||||
summary = await todo.run()
|
||||
summary_filename = Path(todo.context.design_filename).with_suffix(".md").name
|
||||
dependencies = {todo.context.design_filename, todo.context.task_filename}
|
||||
for filename in todo.context.codes_filenames:
|
||||
rpath = src_relative_path / filename
|
||||
dependencies.add(str(rpath))
|
||||
await code_summaries_pdf_file_repo.save(
|
||||
filename=summary_filename, content=summary, dependencies=dependencies
|
||||
)
|
||||
is_pass, reason = await self._is_pass(summary)
|
||||
if not is_pass:
|
||||
todo.context.reason = reason
|
||||
tasks.append(todo.context.dict())
|
||||
await code_summaries_file_repo.save(
|
||||
filename=Path(todo.context.design_filename).name,
|
||||
content=todo.context.json(),
|
||||
dependencies=dependencies,
|
||||
)
|
||||
else:
|
||||
await code_summaries_file_repo.delete(filename=Path(todo.context.design_filename).name)
|
||||
|
||||
logger.info(f"--max-auto-summarize-code={CONFIG.max_auto_summarize_code}")
|
||||
if not tasks or CONFIG.max_auto_summarize_code == 0:
|
||||
return Message(
|
||||
content="",
|
||||
role=self.profile,
|
||||
cause_by=SummarizeCode,
|
||||
sent_from=self,
|
||||
send_to="Edward", # The name of QaEngineer
|
||||
)
|
||||
# The maximum number of times the 'SummarizeCode' action is automatically invoked, with -1 indicating unlimited.
|
||||
# This parameter is used for debugging the workflow.
|
||||
CONFIG.max_auto_summarize_code -= 1 if CONFIG.max_auto_summarize_code > 0 else 0
|
||||
return Message(
|
||||
content=json.dumps(tasks), role=self.profile, cause_by=SummarizeCode, send_to=self, sent_from=self
|
||||
)
|
||||
|
||||
async def _is_pass(self, summary) -> (str, str):
|
||||
rsp = await self._llm.aask(msg=IS_PASS_PROMPT.format(context=summary), stream=False)
|
||||
logger.info(rsp)
|
||||
if "YES" in rsp:
|
||||
return True, rsp
|
||||
return False, rsp
|
||||
|
||||
async def _think(self) -> Action | None:
|
||||
if not CONFIG.src_workspace:
|
||||
CONFIG.src_workspace = CONFIG.git_repo.workdir / CONFIG.git_repo.workdir.name
|
||||
write_code_filters = any_to_str_set([WriteTasks, SummarizeCode, FixBug])
|
||||
summarize_code_filters = any_to_str_set([WriteCode, WriteCodeReview])
|
||||
if not self._rc.news:
|
||||
return None
|
||||
msg = self._rc.news[0]
|
||||
if msg.cause_by in write_code_filters:
|
||||
logger.debug(f"TODO WriteCode:{msg.json()}")
|
||||
await self._new_code_actions(bug_fix=msg.cause_by == any_to_str(FixBug))
|
||||
return self._rc.todo
|
||||
if msg.cause_by in summarize_code_filters and msg.sent_from == any_to_str(self):
|
||||
logger.debug(f"TODO SummarizeCode:{msg.json()}")
|
||||
await self._new_summarize_actions()
|
||||
return self._rc.todo
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def _new_coding_context(
|
||||
filename, src_file_repo, task_file_repo, design_file_repo, dependency
|
||||
) -> CodingContext:
|
||||
old_code_doc = await src_file_repo.get(filename)
|
||||
if not old_code_doc:
|
||||
old_code_doc = Document(root_path=str(src_file_repo.root_path), filename=filename, content="")
|
||||
dependencies = {Path(i) for i in await dependency.get(old_code_doc.root_relative_path)}
|
||||
task_doc = None
|
||||
design_doc = None
|
||||
for i in dependencies:
|
||||
if str(i.parent) == TASK_FILE_REPO:
|
||||
task_doc = await task_file_repo.get(i.name)
|
||||
elif str(i.parent) == SYSTEM_DESIGN_FILE_REPO:
|
||||
design_doc = await design_file_repo.get(i.name)
|
||||
# FIXME: design doc没有加载进来,是None
|
||||
context = CodingContext(filename=filename, design_doc=design_doc, task_doc=task_doc, code_doc=old_code_doc)
|
||||
return context
|
||||
|
||||
@staticmethod
|
||||
async def _new_coding_doc(filename, src_file_repo, task_file_repo, design_file_repo, dependency):
|
||||
context = await Engineer._new_coding_context(
|
||||
filename, src_file_repo, task_file_repo, design_file_repo, dependency
|
||||
)
|
||||
coding_doc = Document(root_path=str(src_file_repo.root_path), filename=filename, content=context.json())
|
||||
return coding_doc
|
||||
|
||||
async def _new_code_actions(self, bug_fix=False):
|
||||
# Prepare file repos
|
||||
src_file_repo = CONFIG.git_repo.new_file_repository(CONFIG.src_workspace)
|
||||
changed_src_files = src_file_repo.all_files if bug_fix else src_file_repo.changed_files
|
||||
task_file_repo = CONFIG.git_repo.new_file_repository(TASK_FILE_REPO)
|
||||
changed_task_files = task_file_repo.changed_files
|
||||
design_file_repo = CONFIG.git_repo.new_file_repository(SYSTEM_DESIGN_FILE_REPO)
|
||||
|
||||
changed_files = Documents()
|
||||
# Recode caused by upstream changes.
|
||||
for filename in changed_task_files:
|
||||
design_doc = await design_file_repo.get(filename)
|
||||
task_doc = await task_file_repo.get(filename)
|
||||
task_list = self._parse_tasks(task_doc)
|
||||
for task_filename in task_list:
|
||||
old_code_doc = await src_file_repo.get(task_filename)
|
||||
if not old_code_doc:
|
||||
old_code_doc = Document(root_path=str(src_file_repo.root_path), filename=task_filename, content="")
|
||||
context = CodingContext(
|
||||
filename=task_filename, design_doc=design_doc, task_doc=task_doc, code_doc=old_code_doc
|
||||
)
|
||||
coding_doc = Document(
|
||||
root_path=str(src_file_repo.root_path), filename=task_filename, content=context.json()
|
||||
)
|
||||
if task_filename in changed_files.docs:
|
||||
logger.warning(
|
||||
f"Log to expose potential conflicts: {coding_doc.json()} & "
|
||||
f"{changed_files.docs[task_filename].json()}"
|
||||
)
|
||||
changed_files.docs[task_filename] = coding_doc
|
||||
self.code_todos = [WriteCode(context=i, llm=self._llm) for i in changed_files.docs.values()]
|
||||
# Code directly modified by the user.
|
||||
dependency = await CONFIG.git_repo.get_dependency()
|
||||
for filename in changed_src_files:
|
||||
if filename in changed_files.docs:
|
||||
continue
|
||||
coding_doc = await self._new_coding_doc(
|
||||
filename=filename,
|
||||
src_file_repo=src_file_repo,
|
||||
task_file_repo=task_file_repo,
|
||||
design_file_repo=design_file_repo,
|
||||
dependency=dependency,
|
||||
)
|
||||
changed_files.docs[filename] = coding_doc
|
||||
self.code_todos.append(WriteCode(context=coding_doc, llm=self._llm))
|
||||
|
||||
if self.code_todos:
|
||||
self._rc.todo = self.code_todos[0]
|
||||
|
||||
async def _new_summarize_actions(self):
|
||||
src_file_repo = CONFIG.git_repo.new_file_repository(CONFIG.src_workspace)
|
||||
src_files = src_file_repo.all_files
|
||||
# Generate a SummarizeCode action for each pair of (system_design_doc, task_doc).
|
||||
summarizations = defaultdict(list)
|
||||
for filename in src_files:
|
||||
dependencies = await src_file_repo.get_dependency(filename=filename)
|
||||
ctx = CodeSummarizeContext.loads(filenames=dependencies)
|
||||
summarizations[ctx].append(filename)
|
||||
for ctx, filenames in summarizations.items():
|
||||
ctx.codes_filenames = filenames
|
||||
self.summarize_todos.append(SummarizeCode(context=ctx, llm=self._llm))
|
||||
if self.summarize_todos:
|
||||
self._rc.todo = self.summarize_todos[0]
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import pandas as pd
|
||||
|
||||
from metagpt.actions.invoice_ocr import InvoiceOCR, GenerateTable, ReplyQuestion
|
||||
from metagpt.actions.invoice_ocr import GenerateTable, InvoiceOCR, ReplyQuestion
|
||||
from metagpt.prompts.invoice_ocr import INVOICE_OCR_SUCCESS
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import Message
|
||||
|
|
|
|||
|
|
@ -4,9 +4,14 @@
|
|||
@Time : 2023/5/11 14:43
|
||||
@Author : alexanderwu
|
||||
@File : product_manager.py
|
||||
@Modified By: mashenquan, 2023/11/27. Add `PrepareDocuments` action according to Section 2.2.3.5.1 of RFC 135.
|
||||
"""
|
||||
from metagpt.actions import BossRequirement, WritePRD
|
||||
from metagpt.roles import Role
|
||||
|
||||
|
||||
from metagpt.actions import UserRequirement, WritePRD
|
||||
from metagpt.actions.prepare_documents import PrepareDocuments
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.roles.role import Role
|
||||
|
||||
|
||||
class ProductManager(Role):
|
||||
|
|
@ -20,22 +25,24 @@ class ProductManager(Role):
|
|||
constraints (str): Constraints or limitations for the product manager.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "Alice",
|
||||
profile: str = "Product Manager",
|
||||
goal: str = "Efficiently create a successful product",
|
||||
constraints: str = "",
|
||||
) -> None:
|
||||
"""
|
||||
Initializes the ProductManager role with given attributes.
|
||||
name: str = "Alice"
|
||||
profile: str = "Product Manager"
|
||||
goal: str = "efficiently create a successful product that meets market demands and user expectations"
|
||||
constraints: str = "utilize the same language as the user requirements for seamless communication"
|
||||
|
||||
Args:
|
||||
name (str): Name of the product manager.
|
||||
profile (str): Role profile.
|
||||
goal (str): Goal of the product manager.
|
||||
constraints (str): Constraints or limitations for the product manager.
|
||||
"""
|
||||
super().__init__(name, profile, goal, constraints)
|
||||
self._init_actions([WritePRD])
|
||||
self._watch([BossRequirement])
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._init_actions([PrepareDocuments, WritePRD])
|
||||
self._watch([UserRequirement, PrepareDocuments])
|
||||
|
||||
async def _think(self) -> None:
|
||||
"""Decide what to do"""
|
||||
if CONFIG.git_repo:
|
||||
self._set_state(1)
|
||||
else:
|
||||
self._set_state(0)
|
||||
return self._rc.todo
|
||||
|
||||
async def _observe(self, ignore_memory=False) -> int:
|
||||
return await super()._observe(ignore_memory=True)
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@
|
|||
@Author : alexanderwu
|
||||
@File : project_manager.py
|
||||
"""
|
||||
|
||||
from metagpt.actions import WriteTasks
|
||||
from metagpt.actions.design_api import WriteDesign
|
||||
from metagpt.roles import Role
|
||||
from metagpt.roles.role import Role
|
||||
|
||||
|
||||
class ProjectManager(Role):
|
||||
|
|
@ -21,22 +22,16 @@ class ProjectManager(Role):
|
|||
constraints (str): Constraints or limitations for the project manager.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "Eve",
|
||||
profile: str = "Project Manager",
|
||||
goal: str = "Improve team efficiency and deliver with quality and quantity",
|
||||
constraints: str = "",
|
||||
) -> None:
|
||||
"""
|
||||
Initializes the ProjectManager role with given attributes.
|
||||
name: str = "Eve"
|
||||
profile: str = "Project Manager"
|
||||
goal: str = (
|
||||
"break down tasks according to PRD/technical design, generate a task list, and analyze task "
|
||||
"dependencies to start with the prerequisite modules"
|
||||
)
|
||||
constraints: str = "use same language as user requirement"
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
Args:
|
||||
name (str): Name of the project manager.
|
||||
profile (str): Role profile.
|
||||
goal (str): Goal of the project manager.
|
||||
constraints (str): Constraints or limitations for the project manager.
|
||||
"""
|
||||
super().__init__(name, profile, goal, constraints)
|
||||
self._init_actions([WriteTasks])
|
||||
self._watch([WriteDesign])
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ SUFFIX = """Let's begin!
|
|||
Question: {input}
|
||||
Thoughts: {agent_scratchpad}"""
|
||||
|
||||
|
||||
class PromptString(Enum):
|
||||
REFLECTION_QUESTIONS = "Here are some statements:\n{memory_descriptions}\n\nBased solely on the information above, what are the 3 most prominent high-level questions we can answer about the topic in the statements?\n\n{format_instructions}"
|
||||
|
||||
|
|
@ -32,7 +33,7 @@ class PromptString(Enum):
|
|||
|
||||
RECENT_ACTIVITY = "Based on the following memory, produce a brief summary of what {full_name} has been up to recently. Do not invent details not explicitly stated in the memory. For any conversation, be sure to mention whether the conversation has concluded or is still ongoing.\n\nMemory: {memory_descriptions}"
|
||||
|
||||
MAKE_PLANS = "You are a plan-generating AI. Your job is to assist the character in formulating new plans based on new information. Given the character's information (profile, objectives, recent activities, current plans, and location context) and their current thought process, produce a new set of plans for them. The final plan should comprise at least {time_window} of activities and no more than 5 individual plans. List the plans in the order they should be executed, with each plan detailing its description, location, start time, stop criteria, and maximum duration.\n\nSample plan: {{\"index\": 1, \"description\": \"Cook dinner\", \"location_id\": \"0a3bc22b-36aa-48ab-adb0-18616004caed\",\"start_time\": \"2022-12-12T20:00:00+00:00\",\"max_duration_hrs\": 1.5, \"stop_condition\": \"Dinner is fully prepared\"}}\'\n\nFor each plan, choose the most appropriate location name from this list: {allowed_location_descriptions}\n\n{format_instructions}\n\nAlways prioritize completing any unfinished conversations.\n\nLet's begin!\n\nName: {full_name}\nProfile: {private_bio}\nObjectives: {directives}\nLocation Context: {location_context}\nCurrent Plans: {current_plans}\nRecent Activities: {recent_activity}\nThought Process: {thought_process}\nIt's essential to encourage the character to collaborate with other characters in their plans.\n\n"
|
||||
MAKE_PLANS = 'You are a plan-generating AI. Your job is to assist the character in formulating new plans based on new information. Given the character\'s information (profile, objectives, recent activities, current plans, and location context) and their current thought process, produce a new set of plans for them. The final plan should comprise at least {time_window} of activities and no more than 5 individual plans. List the plans in the order they should be executed, with each plan detailing its description, location, start time, stop criteria, and maximum duration.\n\nSample plan: {{"index": 1, "description": "Cook dinner", "location_id": "0a3bc22b-36aa-48ab-adb0-18616004caed","start_time": "2022-12-12T20:00:00+00:00","max_duration_hrs": 1.5, "stop_condition": "Dinner is fully prepared"}}\'\n\nFor each plan, choose the most appropriate location name from this list: {allowed_location_descriptions}\n\n{format_instructions}\n\nAlways prioritize completing any unfinished conversations.\n\nLet\'s begin!\n\nName: {full_name}\nProfile: {private_bio}\nObjectives: {directives}\nLocation Context: {location_context}\nCurrent Plans: {current_plans}\nRecent Activities: {recent_activity}\nThought Process: {thought_process}\nIt\'s essential to encourage the character to collaborate with other characters in their plans.\n\n'
|
||||
|
||||
EXECUTE_PLAN = "You are a role-playing AI, playing the role of {your_name}, in front of a live audience. Every word you say can be observed by the audience, so make sure you speak often and make it interesting. You cannot interact directly with the audience.\n\nBased on the following context and tools, proceed as if you are {your_name}. Your primary task is to complete the task given below, however, if you are currently in a conversation with another character, you should always complete the conversation first and then proceed with the task. Do not start working while you are engaged in an unfinished conversation. Use your best judgment to determine whether a conversation involves you and whether it is unfinished. You don't need to respond to every message you receive.\n\nThis is a personal profile of your character:\n{your_private_bio}\n\nThis is a description of your location and other nearby characters you can converse with:\n\n{location_context}\n\nThese are some relevant memories:\n```\n{relevant_memories}\n```\n\nThis is some relevant conversation history:\n```\n{conversation_history}\n```\n\nThese are the tools you can use:\n{tools}\n\nYour response should follow the format below:\n\nTask: The task you must complete\nThoughts: What you should think about doing\nAction: The action to take, must be one of these words: [{tool_names}]\nAction Input: Input for the action\nObservation: The result of the action\n... (This Thoughts/Action/Action Input/Observation can be repeated N times)\nThoughts: 'I have completed the task'\nFinal Response: The final response to the task\n\nIf you are not ready with a final response, then you must take an action.\n\nIf you are certain that you cannot complete the task with the tools provided, return 'Final Response: Need Help', however, if you are in conversation with another character, responses like 'I don't know' are valid. During conversations, you should never break character or admit you are an AI.\nIf the task is completed and no specific response is required, return 'Final Response: Done'\nLet's begin!\n\nTask: {input}\n\n{agent_scratchpad}"
|
||||
|
||||
|
|
@ -40,6 +41,6 @@ class PromptString(Enum):
|
|||
|
||||
GOSSIP = "You are {full_name}. \n{memory_descriptions}\n\nBased on the statements above, say a thing or two of interest to others at your location: {other_agent_names}.\nAlways specify their names when referring to others."
|
||||
|
||||
HAS_HAPPENED = "Given the descriptions of the observations of the following characters and the events they are awaiting, indicate whether the character has witnessed the event.\n{format_instructions}\n\nExample:\n\nObservations:\nJoe entered the office at 2023-05-04 08:00:00+00:00\nJoe said hi to Sally at 2023-05-04 08:05:00+00:00\nSally said hello to Joe at 2023-05-04 08:05:30+00:00\nRebecca started working at 2023-05-04 08:10:00+00:00\nJoe made some breakfast at 2023-05-04 08:15:00+00:00\n\nAwaiting: Sally responded to Joe\n\nYour response: '{{\"has_happened\": true, \"date_occured\": 2023-05-04 08:05:30+00:00}}'\n\nLet's begin!\n\nObservations:\n{memory_descriptions}\n\nAwaiting: {event_description}\n"
|
||||
HAS_HAPPENED = 'Given the descriptions of the observations of the following characters and the events they are awaiting, indicate whether the character has witnessed the event.\n{format_instructions}\n\nExample:\n\nObservations:\nJoe entered the office at 2023-05-04 08:00:00+00:00\nJoe said hi to Sally at 2023-05-04 08:05:00+00:00\nSally said hello to Joe at 2023-05-04 08:05:30+00:00\nRebecca started working at 2023-05-04 08:10:00+00:00\nJoe made some breakfast at 2023-05-04 08:15:00+00:00\n\nAwaiting: Sally responded to Joe\n\nYour response: \'{{"has_happened": true, "date_occured": 2023-05-04 08:05:30+00:00}}\'\n\nLet\'s begin!\n\nObservations:\n{memory_descriptions}\n\nAwaiting: {event_description}\n'
|
||||
|
||||
OUTPUT_FORMAT = "\n\n(Remember! Make sure your output always adheres to one of the following two formats:\n\nA. If you have completed the task:\nThoughts: 'I have completed the task'\nFinal Response: <str>\n\nB. If you haven't completed the task:\nThoughts: <str>\nAction: <str>\nAction Input: <str>\nObservation: <str>)\n"
|
||||
|
|
|
|||
|
|
@ -4,153 +4,145 @@
|
|||
@Time : 2023/5/11 14:43
|
||||
@Author : alexanderwu
|
||||
@File : qa_engineer.py
|
||||
@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, modify the data
|
||||
type of the `cause_by` value in the `Message` to a string, and utilize the new message filtering feature.
|
||||
@Modified By: mashenquan, 2023-11-27.
|
||||
1. Following the think-act principle, solidify the task parameters when creating the
|
||||
WriteTest/RunCode/DebugError object, rather than passing them in when calling the run function.
|
||||
2. According to Section 2.2.3.5.7 of RFC 135, change the method of transferring files from using the Message
|
||||
to using file references.
|
||||
@Modified By: mashenquan, 2023-12-5. Enhance the workflow to navigate to WriteCode or QaEngineer based on the results
|
||||
of SummarizeCode.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from metagpt.actions import (
|
||||
DebugError,
|
||||
RunCode,
|
||||
WriteCode,
|
||||
WriteCodeReview,
|
||||
WriteDesign,
|
||||
WriteTest,
|
||||
|
||||
from metagpt.actions import DebugError, RunCode, WriteTest
|
||||
from metagpt.actions.summarize_code import SummarizeCode
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.const import (
|
||||
MESSAGE_ROUTE_TO_NONE,
|
||||
TEST_CODES_FILE_REPO,
|
||||
TEST_OUTPUTS_FILE_REPO,
|
||||
)
|
||||
from metagpt.const import WORKSPACE_ROOT
|
||||
from metagpt.logs import logger
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import Message
|
||||
from metagpt.utils.common import CodeParser, parse_recipient
|
||||
from metagpt.utils.special_tokens import FILENAME_CODE_SEP, MSG_SEP
|
||||
from metagpt.schema import Document, Message, RunCodeContext, TestingContext
|
||||
from metagpt.utils.common import any_to_str_set, parse_recipient
|
||||
from metagpt.utils.file_repository import FileRepository
|
||||
|
||||
|
||||
class QaEngineer(Role):
|
||||
def __init__(
|
||||
self,
|
||||
name="Edward",
|
||||
profile="QaEngineer",
|
||||
goal="Write comprehensive and robust tests to ensure codes will work as expected without bugs",
|
||||
constraints="The test code you write should conform to code standard like PEP8, be modular, easy to read and maintain",
|
||||
test_round_allowed=5,
|
||||
):
|
||||
super().__init__(name, profile, goal, constraints)
|
||||
self._init_actions(
|
||||
[WriteTest]
|
||||
) # FIXME: a bit hack here, only init one action to circumvent _think() logic, will overwrite _think() in future updates
|
||||
self._watch([WriteCode, WriteCodeReview, WriteTest, RunCode, DebugError])
|
||||
name: str = "Edward"
|
||||
profile: str = "QaEngineer"
|
||||
goal: str = "Write comprehensive and robust tests to ensure codes will work as expected without bugs"
|
||||
constraints: str = (
|
||||
"The test code you write should conform to code standard like PEP8, be modular, " "easy to read and maintain"
|
||||
)
|
||||
test_round_allowed: int = 5
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# FIXME: a bit hack here, only init one action to circumvent _think() logic,
|
||||
# will overwrite _think() in future updates
|
||||
self._init_actions([WriteTest])
|
||||
self._watch([SummarizeCode, WriteTest, RunCode, DebugError])
|
||||
self.test_round = 0
|
||||
self.test_round_allowed = test_round_allowed
|
||||
|
||||
@classmethod
|
||||
def parse_workspace(cls, system_design_msg: Message) -> str:
|
||||
if system_design_msg.instruct_content:
|
||||
return system_design_msg.instruct_content.dict().get("Python package name")
|
||||
return CodeParser.parse_str(block="Python package name", text=system_design_msg.content)
|
||||
|
||||
def get_workspace(self, return_proj_dir=True) -> Path:
|
||||
msg = self._rc.memory.get_by_action(WriteDesign)[-1]
|
||||
if not msg:
|
||||
return WORKSPACE_ROOT / "src"
|
||||
workspace = self.parse_workspace(msg)
|
||||
# project directory: workspace/{package_name}, which contains package source code folder, tests folder, resources folder, etc.
|
||||
if return_proj_dir:
|
||||
return WORKSPACE_ROOT / workspace
|
||||
# development codes directory: workspace/{package_name}/{package_name}
|
||||
return WORKSPACE_ROOT / workspace / workspace
|
||||
|
||||
def write_file(self, filename: str, code: str):
|
||||
workspace = self.get_workspace() / "tests"
|
||||
file = workspace / filename
|
||||
file.parent.mkdir(parents=True, exist_ok=True)
|
||||
file.write_text(code)
|
||||
|
||||
async def _write_test(self, message: Message) -> None:
|
||||
code_msgs = message.content.split(MSG_SEP)
|
||||
# result_msg_all = []
|
||||
for code_msg in code_msgs:
|
||||
src_file_repo = CONFIG.git_repo.new_file_repository(CONFIG.src_workspace)
|
||||
changed_files = set(src_file_repo.changed_files.keys())
|
||||
# Unit tests only.
|
||||
if CONFIG.reqa_file and CONFIG.reqa_file not in changed_files:
|
||||
changed_files.add(CONFIG.reqa_file)
|
||||
tests_file_repo = CONFIG.git_repo.new_file_repository(TEST_CODES_FILE_REPO)
|
||||
for filename in changed_files:
|
||||
# write tests
|
||||
file_name, file_path = code_msg.split(FILENAME_CODE_SEP)
|
||||
code_to_test = open(file_path, "r").read()
|
||||
if "test" in file_name:
|
||||
continue # Engineer might write some test files, skip testing a test file
|
||||
test_file_name = "test_" + file_name
|
||||
test_file_path = self.get_workspace() / "tests" / test_file_name
|
||||
logger.info(f"Writing {test_file_name}..")
|
||||
test_code = await WriteTest().run(
|
||||
code_to_test=code_to_test,
|
||||
test_file_name=test_file_name,
|
||||
# source_file_name=file_name,
|
||||
source_file_path=file_path,
|
||||
workspace=self.get_workspace(),
|
||||
if not filename or "test" in filename:
|
||||
continue
|
||||
code_doc = await src_file_repo.get(filename)
|
||||
test_doc = await tests_file_repo.get("test_" + code_doc.filename)
|
||||
if not test_doc:
|
||||
test_doc = Document(
|
||||
root_path=str(tests_file_repo.root_path), filename="test_" + code_doc.filename, content=""
|
||||
)
|
||||
logger.info(f"Writing {test_doc.filename}..")
|
||||
context = TestingContext(filename=test_doc.filename, test_doc=test_doc, code_doc=code_doc)
|
||||
context = await WriteTest(context=context, llm=self._llm).run()
|
||||
await tests_file_repo.save(
|
||||
filename=context.test_doc.filename,
|
||||
content=context.test_doc.content,
|
||||
dependencies={context.code_doc.root_relative_path},
|
||||
)
|
||||
self.write_file(test_file_name, test_code)
|
||||
|
||||
# prepare context for run tests in next round
|
||||
command = ["python", f"tests/{test_file_name}"]
|
||||
file_info = {
|
||||
"file_name": file_name,
|
||||
"file_path": str(file_path),
|
||||
"test_file_name": test_file_name,
|
||||
"test_file_path": str(test_file_path),
|
||||
"command": command,
|
||||
}
|
||||
msg = Message(
|
||||
content=str(file_info),
|
||||
role=self.profile,
|
||||
cause_by=WriteTest,
|
||||
sent_from=self.profile,
|
||||
send_to=self.profile,
|
||||
run_code_context = RunCodeContext(
|
||||
command=["python", context.test_doc.root_relative_path],
|
||||
code_filename=context.code_doc.filename,
|
||||
test_filename=context.test_doc.filename,
|
||||
working_directory=str(CONFIG.git_repo.workdir),
|
||||
additional_python_paths=[str(CONFIG.src_workspace)],
|
||||
)
|
||||
self.publish_message(
|
||||
Message(
|
||||
content=run_code_context.json(),
|
||||
role=self.profile,
|
||||
cause_by=WriteTest,
|
||||
sent_from=self,
|
||||
send_to=self,
|
||||
)
|
||||
)
|
||||
self._publish_message(msg)
|
||||
|
||||
logger.info(f"Done {self.get_workspace()}/tests generating.")
|
||||
logger.info(f"Done {str(tests_file_repo.workdir)} generating.")
|
||||
|
||||
async def _run_code(self, msg):
|
||||
file_info = eval(msg.content)
|
||||
development_file_path = file_info["file_path"]
|
||||
test_file_path = file_info["test_file_path"]
|
||||
if not os.path.exists(development_file_path) or not os.path.exists(test_file_path):
|
||||
run_code_context = RunCodeContext.loads(msg.content)
|
||||
src_doc = await CONFIG.git_repo.new_file_repository(CONFIG.src_workspace).get(run_code_context.code_filename)
|
||||
if not src_doc:
|
||||
return
|
||||
|
||||
development_code = open(development_file_path, "r").read()
|
||||
test_code = open(test_file_path, "r").read()
|
||||
proj_dir = self.get_workspace()
|
||||
development_code_dir = self.get_workspace(return_proj_dir=False)
|
||||
|
||||
result_msg = await RunCode().run(
|
||||
mode="script",
|
||||
code=development_code,
|
||||
code_file_name=file_info["file_name"],
|
||||
test_code=test_code,
|
||||
test_file_name=file_info["test_file_name"],
|
||||
command=file_info["command"],
|
||||
working_directory=proj_dir, # workspace/package_name, will run tests/test_xxx.py here
|
||||
additional_python_paths=[development_code_dir], # workspace/package_name/package_name,
|
||||
# import statement inside package code needs this
|
||||
test_doc = await CONFIG.git_repo.new_file_repository(TEST_CODES_FILE_REPO).get(run_code_context.test_filename)
|
||||
if not test_doc:
|
||||
return
|
||||
run_code_context.code = src_doc.content
|
||||
run_code_context.test_code = test_doc.content
|
||||
result = await RunCode(context=run_code_context, llm=self._llm).run()
|
||||
run_code_context.output_filename = run_code_context.test_filename + ".json"
|
||||
await CONFIG.git_repo.new_file_repository(TEST_OUTPUTS_FILE_REPO).save(
|
||||
filename=run_code_context.output_filename,
|
||||
content=result.json(),
|
||||
dependencies={src_doc.root_relative_path, test_doc.root_relative_path},
|
||||
)
|
||||
run_code_context.code = None
|
||||
run_code_context.test_code = None
|
||||
# the recipient might be Engineer or myself
|
||||
recipient = parse_recipient(result.summary)
|
||||
mappings = {"Engineer": "Alex", "QaEngineer": "Edward"}
|
||||
self.publish_message(
|
||||
Message(
|
||||
content=run_code_context.json(),
|
||||
role=self.profile,
|
||||
cause_by=RunCode,
|
||||
sent_from=self,
|
||||
send_to=mappings.get(recipient, MESSAGE_ROUTE_TO_NONE),
|
||||
)
|
||||
)
|
||||
|
||||
recipient = parse_recipient(result_msg) # the recipient might be Engineer or myself
|
||||
content = str(file_info) + FILENAME_CODE_SEP + result_msg
|
||||
msg = Message(content=content, role=self.profile, cause_by=RunCode, sent_from=self.profile, send_to=recipient)
|
||||
self._publish_message(msg)
|
||||
|
||||
async def _debug_error(self, msg):
|
||||
file_info, context = msg.content.split(FILENAME_CODE_SEP)
|
||||
file_name, code = await DebugError().run(context)
|
||||
if file_name:
|
||||
self.write_file(file_name, code)
|
||||
recipient = msg.sent_from # send back to the one who ran the code for another run, might be one's self
|
||||
msg = Message(
|
||||
content=file_info, role=self.profile, cause_by=DebugError, sent_from=self.profile, send_to=recipient
|
||||
run_code_context = RunCodeContext.loads(msg.content)
|
||||
code = await DebugError(context=run_code_context, llm=self._llm).run()
|
||||
await FileRepository.save_file(
|
||||
filename=run_code_context.test_filename, content=code, relative_path=TEST_CODES_FILE_REPO
|
||||
)
|
||||
run_code_context.output = None
|
||||
self.publish_message(
|
||||
Message(
|
||||
content=run_code_context.json(),
|
||||
role=self.profile,
|
||||
cause_by=DebugError,
|
||||
sent_from=self,
|
||||
send_to=self,
|
||||
)
|
||||
self._publish_message(msg)
|
||||
|
||||
async def _observe(self) -> int:
|
||||
await super()._observe()
|
||||
self._rc.news = [
|
||||
msg for msg in self._rc.news if msg.send_to == self.profile
|
||||
] # only relevant msgs count as observed news
|
||||
return len(self._rc.news)
|
||||
)
|
||||
|
||||
async def _act(self) -> Message:
|
||||
if self.test_round > self.test_round_allowed:
|
||||
|
|
@ -159,28 +151,35 @@ class QaEngineer(Role):
|
|||
role=self.profile,
|
||||
cause_by=WriteTest,
|
||||
sent_from=self.profile,
|
||||
send_to="",
|
||||
send_to=MESSAGE_ROUTE_TO_NONE,
|
||||
)
|
||||
return result_msg
|
||||
|
||||
code_filters = any_to_str_set({SummarizeCode})
|
||||
test_filters = any_to_str_set({WriteTest, DebugError})
|
||||
run_filters = any_to_str_set({RunCode})
|
||||
for msg in self._rc.news:
|
||||
# Decide what to do based on observed msg type, currently defined by human,
|
||||
# might potentially be moved to _think, that is, let the agent decides for itself
|
||||
if msg.cause_by in [WriteCode, WriteCodeReview]:
|
||||
if msg.cause_by in code_filters:
|
||||
# engineer wrote a code, time to write a test for it
|
||||
await self._write_test(msg)
|
||||
elif msg.cause_by in [WriteTest, DebugError]:
|
||||
elif msg.cause_by in test_filters:
|
||||
# I wrote or debugged my test code, time to run it
|
||||
await self._run_code(msg)
|
||||
elif msg.cause_by == RunCode:
|
||||
elif msg.cause_by in run_filters:
|
||||
# I ran my test code, time to fix bugs, if any
|
||||
await self._debug_error(msg)
|
||||
self.test_round += 1
|
||||
result_msg = Message(
|
||||
return Message(
|
||||
content=f"Round {self.test_round} of tests done",
|
||||
role=self.profile,
|
||||
cause_by=WriteTest,
|
||||
sent_from=self.profile,
|
||||
send_to="",
|
||||
send_to=MESSAGE_ROUTE_TO_NONE,
|
||||
)
|
||||
return result_msg
|
||||
|
||||
async def _observe(self, ignore_memory=False) -> int:
|
||||
# This role has events that trigger and execute themselves based on conditions, and cannot rely on the
|
||||
# content of memory to activate.
|
||||
return await super()._observe(ignore_memory=True)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
#!/usr/bin/env python
|
||||
"""
|
||||
@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of
|
||||
the `cause_by` value in the `Message` to a string to support the new message distribution feature.
|
||||
"""
|
||||
|
||||
|
||||
import asyncio
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from metagpt.actions import CollectLinks, ConductResearch, WebBrowseAndSummarize
|
||||
from metagpt.actions import Action, CollectLinks, ConductResearch, WebBrowseAndSummarize
|
||||
from metagpt.actions.research import get_research_system_text
|
||||
from metagpt.const import RESEARCH_PATH
|
||||
from metagpt.logs import logger
|
||||
|
|
@ -46,24 +51,36 @@ class Researcher(Role):
|
|||
else:
|
||||
topic = msg.content
|
||||
|
||||
research_system_text = get_research_system_text(topic, self.language)
|
||||
research_system_text = self.research_system_text(topic, todo)
|
||||
if isinstance(todo, CollectLinks):
|
||||
links = await todo.run(topic, 4, 4)
|
||||
ret = Message("", Report(topic=topic, links=links), role=self.profile, cause_by=type(todo))
|
||||
ret = Message("", Report(topic=topic, links=links), role=self.profile, cause_by=todo)
|
||||
elif isinstance(todo, WebBrowseAndSummarize):
|
||||
links = instruct_content.links
|
||||
todos = (todo.run(*url, query=query, system_text=research_system_text) for (query, url) in links.items())
|
||||
summaries = await asyncio.gather(*todos)
|
||||
summaries = list((url, summary) for i in summaries for (url, summary) in i.items() if summary)
|
||||
ret = Message("", Report(topic=topic, summaries=summaries), role=self.profile, cause_by=type(todo))
|
||||
ret = Message("", Report(topic=topic, summaries=summaries), role=self.profile, cause_by=todo)
|
||||
else:
|
||||
summaries = instruct_content.summaries
|
||||
summary_text = "\n---\n".join(f"url: {url}\nsummary: {summary}" for (url, summary) in summaries)
|
||||
content = await self._rc.todo.run(topic, summary_text, system_text=research_system_text)
|
||||
ret = Message("", Report(topic=topic, content=content), role=self.profile, cause_by=type(self._rc.todo))
|
||||
ret = Message("", Report(topic=topic, content=content), role=self.profile, cause_by=self._rc.todo)
|
||||
self._rc.memory.add(ret)
|
||||
return ret
|
||||
|
||||
def research_system_text(self, topic, current_task: Action) -> str:
|
||||
""" BACKWARD compatible
|
||||
This allows sub-class able to define its own system prompt based on topic.
|
||||
return the previous implementation to have backward compatible
|
||||
Args:
|
||||
topic:
|
||||
language:
|
||||
|
||||
Returns: str
|
||||
"""
|
||||
return get_research_system_text(topic, self.language)
|
||||
|
||||
async def react(self) -> Message:
|
||||
msg = await super().react()
|
||||
report = msg.instruct_content
|
||||
|
|
|
|||
|
|
@ -4,21 +4,47 @@
|
|||
@Time : 2023/5/11 14:42
|
||||
@Author : alexanderwu
|
||||
@File : role.py
|
||||
@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116:
|
||||
1. Merge the `recv` functionality into the `_observe` function. Future message reading operations will be
|
||||
consolidated within the `_observe` function.
|
||||
2. Standardize the message filtering for string label matching. Role objects can access the message labels
|
||||
they've subscribed to through the `subscribed_tags` property.
|
||||
3. Move the message receive buffer from the global variable `self._rc.env.memory` to the role's private variable
|
||||
`self._rc.msg_buffer` for easier message identification and asynchronous appending of messages.
|
||||
4. Standardize the way messages are passed: `publish_message` sends messages out, while `put_message` places
|
||||
messages into the Role object's private message receive buffer. There are no other message transmit methods.
|
||||
5. Standardize the parameters for the `run` function: the `test_message` parameter is used for testing purposes
|
||||
only. In the normal workflow, you should use `publish_message` or `put_message` to transmit messages.
|
||||
@Modified By: mashenquan, 2023-11-4. According to the routing feature plan in Chapter 2.2.3.2 of RFC 113, the routing
|
||||
functionality is to be consolidated into the `Environment` class.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, Type, Union
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Set, Type
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# from metagpt.environment import Environment
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.actions import Action, ActionOutput
|
||||
from metagpt.actions.action import action_subclass_registry
|
||||
from metagpt.actions.action_node import ActionNode
|
||||
from metagpt.actions.add_requirement import UserRequirement
|
||||
from metagpt.const import SERDESER_PATH
|
||||
from metagpt.llm import LLM, HumanProvider
|
||||
from metagpt.logs import logger
|
||||
from metagpt.memory import Memory, LongTermMemory
|
||||
from metagpt.schema import Message
|
||||
from metagpt.memory import Memory
|
||||
from metagpt.provider.base_gpt_api import BaseGPTAPI
|
||||
from metagpt.schema import Message, MessageQueue
|
||||
from metagpt.utils.common import (
|
||||
any_to_str,
|
||||
import_class,
|
||||
read_json_file,
|
||||
role_raise_decorator,
|
||||
write_json_file,
|
||||
)
|
||||
from metagpt.utils.repair_llm_raw_output import extract_state_value_from_output
|
||||
|
||||
PREFIX_TEMPLATE = """You are a {profile}, named {name}, your goal is {goal}, and the constraint is {constraints}. """
|
||||
|
||||
|
|
@ -49,6 +75,7 @@ ROLE_TEMPLATE = """Your response should be based on the previous conversation hi
|
|||
{name}: {result}
|
||||
"""
|
||||
|
||||
|
||||
class RoleReactMode(str, Enum):
|
||||
REACT = "react"
|
||||
BY_ORDER = "by_order"
|
||||
|
|
@ -58,41 +85,35 @@ class RoleReactMode(str, Enum):
|
|||
def values(cls):
|
||||
return [item.value for item in cls]
|
||||
|
||||
class RoleSetting(BaseModel):
|
||||
"""Role Settings"""
|
||||
name: str
|
||||
profile: str
|
||||
goal: str
|
||||
constraints: str
|
||||
desc: str
|
||||
is_human: bool
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}({self.profile})"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
|
||||
class RoleContext(BaseModel):
|
||||
"""Role Runtime Context"""
|
||||
env: 'Environment' = Field(default=None)
|
||||
|
||||
# # env exclude=True to avoid `RecursionError: maximum recursion depth exceeded in comparison`
|
||||
env: "Environment" = Field(default=None, exclude=True)
|
||||
# TODO judge if ser&deser
|
||||
msg_buffer: MessageQueue = Field(
|
||||
default_factory=MessageQueue, exclude=True
|
||||
) # Message Buffer with Asynchronous Updates
|
||||
memory: Memory = Field(default_factory=Memory)
|
||||
long_term_memory: LongTermMemory = Field(default_factory=LongTermMemory)
|
||||
state: int = Field(default=-1) # -1 indicates initial or termination state where todo is None
|
||||
todo: Action = Field(default=None)
|
||||
watch: set[Type[Action]] = Field(default_factory=set)
|
||||
news: list[Type[Message]] = Field(default=[])
|
||||
react_mode: RoleReactMode = RoleReactMode.REACT # see `Role._set_react_mode` for definitions of the following two attributes
|
||||
# long_term_memory: LongTermMemory = Field(default_factory=LongTermMemory)
|
||||
state: int = Field(default=-1) # -1 indicates initial or termination state where todo is None
|
||||
todo: Action = Field(default=None, exclude=True)
|
||||
watch: set[str] = Field(default_factory=set)
|
||||
news: list[Type[Message]] = Field(default=[], exclude=True) # TODO not used
|
||||
react_mode: RoleReactMode = (
|
||||
RoleReactMode.REACT
|
||||
) # see `Role._set_react_mode` for definitions of the following two attributes
|
||||
max_react_loop: int = 1
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def check(self, role_id: str):
|
||||
if hasattr(CONFIG, "long_term_memory") and CONFIG.long_term_memory:
|
||||
self.long_term_memory.recover_memory(role_id, self)
|
||||
self.memory = self.long_term_memory # use memory to act as long_term_memory for unify operation
|
||||
# if hasattr(CONFIG, "long_term_memory") and CONFIG.long_term_memory:
|
||||
# self.long_term_memory.recover_memory(role_id, self)
|
||||
# self.memory = self.long_term_memory # use memory to act as long_term_memory for unify operation
|
||||
pass
|
||||
|
||||
@property
|
||||
def important_memory(self) -> list[Message]:
|
||||
|
|
@ -104,33 +125,157 @@ class RoleContext(BaseModel):
|
|||
return self.memory.get()
|
||||
|
||||
|
||||
class Role:
|
||||
role_subclass_registry = {}
|
||||
|
||||
|
||||
class Role(BaseModel):
|
||||
"""Role/Agent"""
|
||||
|
||||
def __init__(self, name="", profile="", goal="", constraints="", desc="", is_human=False):
|
||||
self._llm = LLM() if not is_human else HumanProvider()
|
||||
self._setting = RoleSetting(name=name, profile=profile, goal=goal,
|
||||
constraints=constraints, desc=desc, is_human=is_human)
|
||||
self._states = []
|
||||
self._actions = []
|
||||
self._role_id = str(self._setting)
|
||||
self._rc = RoleContext()
|
||||
name: str = ""
|
||||
profile: str = ""
|
||||
goal: str = ""
|
||||
constraints: str = ""
|
||||
desc: str = ""
|
||||
is_human: bool = False
|
||||
|
||||
_llm: BaseGPTAPI = Field(default_factory=LLM)
|
||||
_role_id: str = ""
|
||||
_states: list[str] = []
|
||||
_actions: list[Action] = []
|
||||
_rc: RoleContext = Field(default_factory=RoleContext)
|
||||
_subscription: tuple[str] = set()
|
||||
|
||||
# builtin variables
|
||||
recovered: bool = False # to tag if a recovered role
|
||||
latest_observed_msg: Message = None # record the latest observed message when interrupted
|
||||
builtin_class_name: str = ""
|
||||
|
||||
_private_attributes = {
|
||||
"_llm": LLM() if not is_human else HumanProvider(),
|
||||
"_role_id": _role_id,
|
||||
"_states": [],
|
||||
"_actions": [],
|
||||
"_rc": RoleContext(),
|
||||
"_subscription": set(),
|
||||
}
|
||||
|
||||
__hash__ = object.__hash__ # support Role as hashable type in `Environment.members`
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
exclude = ["_llm"]
|
||||
|
||||
def __init__(self, **kwargs: Any):
|
||||
for index in range(len(kwargs.get("_actions", []))):
|
||||
current_action = kwargs["_actions"][index]
|
||||
if isinstance(current_action, dict):
|
||||
item_class_name = current_action.get("builtin_class_name", None)
|
||||
for name, subclass in action_subclass_registry.items():
|
||||
registery_class_name = subclass.__fields__["builtin_class_name"].default
|
||||
if item_class_name == registery_class_name:
|
||||
current_action = subclass(**current_action)
|
||||
break
|
||||
kwargs["_actions"][index] = current_action
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# 关于私有变量的初始化 https://github.com/pydantic/pydantic/issues/655
|
||||
self._private_attributes["_llm"] = LLM() if not self.is_human else HumanProvider()
|
||||
self._private_attributes["_role_id"] = str(self._setting)
|
||||
self._private_attributes["_subscription"] = {any_to_str(self), self.name} if self.name else {any_to_str(self)}
|
||||
|
||||
for key in self._private_attributes.keys():
|
||||
if key in kwargs:
|
||||
object.__setattr__(self, key, kwargs[key])
|
||||
if key == "_rc":
|
||||
_rc = RoleContext(**kwargs["_rc"])
|
||||
object.__setattr__(self, "_rc", _rc)
|
||||
else:
|
||||
if key == "_rc":
|
||||
# # Warning, if use self._private_attributes["_rc"],
|
||||
# # self._rc will be a shared object between roles, so init one or reset it inside `_reset`
|
||||
object.__setattr__(self, key, RoleContext())
|
||||
else:
|
||||
object.__setattr__(self, key, self._private_attributes[key])
|
||||
|
||||
self._llm.system_prompt = self._get_prefix()
|
||||
|
||||
# deserialize child classes dynamically for inherited `role`
|
||||
object.__setattr__(self, "builtin_class_name", self.__class__.__name__)
|
||||
self.__fields__["builtin_class_name"].default = self.__class__.__name__
|
||||
|
||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||
super().__init_subclass__(**kwargs)
|
||||
role_subclass_registry[cls.__name__] = cls
|
||||
|
||||
def _reset(self):
|
||||
self._states = []
|
||||
self._actions = []
|
||||
object.__setattr__(self, "_states", [])
|
||||
object.__setattr__(self, "_actions", [])
|
||||
|
||||
@property
|
||||
def _setting(self):
|
||||
return f"{self.name}({self.profile})"
|
||||
|
||||
def serialize(self, stg_path: Path = None):
|
||||
stg_path = (
|
||||
SERDESER_PATH.joinpath(f"team/environment/roles/{self.__class__.__name__}_{self.name}")
|
||||
if stg_path is None
|
||||
else stg_path
|
||||
)
|
||||
|
||||
role_info = self.dict(exclude={"_rc": {"memory": True, "msg_buffer": True}, "_llm": True})
|
||||
role_info.update({"role_class": self.__class__.__name__, "module_name": self.__module__})
|
||||
role_info_path = stg_path.joinpath("role_info.json")
|
||||
write_json_file(role_info_path, role_info)
|
||||
|
||||
self._rc.memory.serialize(stg_path) # serialize role's memory alone
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, stg_path: Path) -> "Role":
|
||||
"""stg_path = ./storage/team/environment/roles/{role_class}_{role_name}"""
|
||||
role_info_path = stg_path.joinpath("role_info.json")
|
||||
role_info = read_json_file(role_info_path)
|
||||
|
||||
role_class_str = role_info.pop("role_class")
|
||||
module_name = role_info.pop("module_name")
|
||||
role_class = import_class(class_name=role_class_str, module_name=module_name)
|
||||
|
||||
role = role_class(**role_info) # initiate particular Role
|
||||
role.set_recovered(True) # set True to make a tag
|
||||
|
||||
role_memory = Memory.deserialize(stg_path)
|
||||
role.set_memory(role_memory)
|
||||
|
||||
return role
|
||||
|
||||
def _init_action_system_message(self, action: Action):
|
||||
action.set_prefix(self._get_prefix())
|
||||
|
||||
def set_recovered(self, recovered: bool = False):
|
||||
self.recovered = recovered
|
||||
|
||||
def set_memory(self, memory: Memory):
|
||||
self._rc.memory = memory
|
||||
|
||||
def init_actions(self, actions):
|
||||
self._init_actions(actions)
|
||||
|
||||
def _init_actions(self, actions):
|
||||
self._reset()
|
||||
for idx, action in enumerate(actions):
|
||||
if not isinstance(action, Action):
|
||||
i = action("", llm=self._llm)
|
||||
## 默认初始化
|
||||
i = action(name="", llm=self._llm)
|
||||
else:
|
||||
if self._setting.is_human and not isinstance(action.llm, HumanProvider):
|
||||
logger.warning(f"is_human attribute does not take effect,"
|
||||
f"as Role's {str(action)} was initialized using LLM, try passing in Action classes instead of initialized instances")
|
||||
logger.warning(
|
||||
f"is_human attribute does not take effect, "
|
||||
f"as Role's {str(action)} was initialized using LLM, "
|
||||
f"try passing in Action classes instead of initialized instances"
|
||||
)
|
||||
i = action
|
||||
i.set_prefix(self._get_prefix(), self.profile)
|
||||
# i.set_env(self._rc.env)
|
||||
self._init_action_system_message(i)
|
||||
self._actions.append(i)
|
||||
self._states.append(f"{idx}. {action}")
|
||||
|
||||
|
|
@ -156,31 +301,47 @@ class Role:
|
|||
self._rc.max_react_loop = max_react_loop
|
||||
|
||||
def _watch(self, actions: Iterable[Type[Action]]):
|
||||
"""Listen to the corresponding behaviors"""
|
||||
self._rc.watch.update(actions)
|
||||
"""Watch Actions of interest. Role will select Messages caused by these Actions from its personal message
|
||||
buffer during _observe.
|
||||
"""
|
||||
self._rc.watch = {any_to_str(t) for t in actions}
|
||||
# check RoleContext after adding watch actions
|
||||
self._rc.check(self._role_id)
|
||||
|
||||
def subscribe(self, tags: Set[str]):
|
||||
"""Used to receive Messages with certain tags from the environment. Message will be put into personal message
|
||||
buffer to be further processed in _observe. By default, a Role subscribes Messages with a tag of its own name
|
||||
or profile.
|
||||
"""
|
||||
self._subscription = tags
|
||||
if self._rc.env: # According to the routing feature plan in Chapter 2.2.3.2 of RFC 113
|
||||
self._rc.env.set_subscription(self, self._subscription)
|
||||
|
||||
def _set_state(self, state: int):
|
||||
"""Update the current state."""
|
||||
self._rc.state = state
|
||||
logger.debug(self._actions)
|
||||
logger.debug(f"actions={self._actions}, state={state}")
|
||||
self._rc.todo = self._actions[self._rc.state] if state >= 0 else None
|
||||
|
||||
def set_env(self, env: 'Environment'):
|
||||
"""Set the environment in which the role works. The role can talk to the environment and can also receive messages by observing."""
|
||||
def set_env(self, env: "Environment"):
|
||||
"""Set the environment in which the role works. The role can talk to the environment and can also receive
|
||||
messages by observing."""
|
||||
self._rc.env = env
|
||||
if env:
|
||||
env.set_subscription(self, self._subscription)
|
||||
|
||||
@property
|
||||
def profile(self):
|
||||
"""Get the role description (position)"""
|
||||
return self._setting.profile
|
||||
def subscription(self) -> Set:
|
||||
"""The labels for messages to be consumed by the Role object."""
|
||||
return self._subscription
|
||||
|
||||
def _get_prefix(self):
|
||||
"""Get the role prefix"""
|
||||
if self._setting.desc:
|
||||
return self._setting.desc
|
||||
return PREFIX_TEMPLATE.format(**self._setting.dict())
|
||||
if self.desc:
|
||||
return self.desc
|
||||
return PREFIX_TEMPLATE.format(
|
||||
**{"profile": self.profile, "name": self.name, "goal": self.goal, "constraints": self.constraints}
|
||||
)
|
||||
|
||||
async def _think(self) -> None:
|
||||
"""Think about what to do and decide on the next action"""
|
||||
|
|
@ -188,15 +349,25 @@ class Role:
|
|||
# If there is only one action, then only this one can be performed
|
||||
self._set_state(0)
|
||||
return
|
||||
if self.recovered and self._rc.state >= 0:
|
||||
self._set_state(self._rc.state) # action to run from recovered state
|
||||
self.recovered = False # avoid max_react_loop out of work
|
||||
return
|
||||
|
||||
prompt = self._get_prefix()
|
||||
prompt += STATE_TEMPLATE.format(history=self._rc.history, states="\n".join(self._states),
|
||||
n_states=len(self._states) - 1, previous_state=self._rc.state)
|
||||
# print(prompt)
|
||||
prompt += STATE_TEMPLATE.format(
|
||||
history=self._rc.history,
|
||||
states="\n".join(self._states),
|
||||
n_states=len(self._states) - 1,
|
||||
previous_state=self._rc.state,
|
||||
)
|
||||
|
||||
next_state = await self._llm.aask(prompt)
|
||||
next_state = extract_state_value_from_output(next_state)
|
||||
logger.debug(f"{prompt=}")
|
||||
if (not next_state.isdigit() and next_state != "-1") \
|
||||
or int(next_state) not in range(-1, len(self._states)):
|
||||
logger.warning(f'Invalid answer of state, {next_state=}, will be set to -1')
|
||||
|
||||
if (not next_state.isdigit() and next_state != "-1") or int(next_state) not in range(-1, len(self._states)):
|
||||
logger.warning(f"Invalid answer of state, {next_state=}, will be set to -1")
|
||||
next_state = -1
|
||||
else:
|
||||
next_state = int(next_state)
|
||||
|
|
@ -205,55 +376,79 @@ class Role:
|
|||
self._set_state(next_state)
|
||||
|
||||
async def _act(self) -> Message:
|
||||
# prompt = self.get_prefix()
|
||||
# prompt += ROLE_TEMPLATE.format(name=self.profile, state=self.states[self.state], result=response,
|
||||
# history=self.history)
|
||||
|
||||
logger.info(f"{self._setting}: ready to {self._rc.todo}")
|
||||
response = await self._rc.todo.run(self._rc.important_memory)
|
||||
# logger.info(response)
|
||||
if isinstance(response, ActionOutput):
|
||||
msg = Message(content=response.content, instruct_content=response.instruct_content,
|
||||
role=self.profile, cause_by=type(self._rc.todo))
|
||||
if isinstance(response, (ActionOutput, ActionNode)):
|
||||
msg = Message(
|
||||
content=response.content,
|
||||
instruct_content=response.instruct_content,
|
||||
role=self.profile,
|
||||
cause_by=self._rc.todo,
|
||||
sent_from=self,
|
||||
)
|
||||
elif isinstance(response, Message):
|
||||
msg = response
|
||||
else:
|
||||
msg = Message(content=response, role=self.profile, cause_by=type(self._rc.todo))
|
||||
msg = Message(content=response, role=self.profile, cause_by=self._rc.todo, sent_from=self)
|
||||
self._rc.memory.add(msg)
|
||||
# logger.debug(f"{response}")
|
||||
|
||||
return msg
|
||||
|
||||
async def _observe(self) -> int:
|
||||
"""Observe from the environment, obtain important information, and add it to memory"""
|
||||
if not self._rc.env:
|
||||
return 0
|
||||
env_msgs = self._rc.env.memory.get()
|
||||
def _find_news(self, observed: list[Message], existed: list[Message]) -> list[Message]:
|
||||
news = []
|
||||
# Warning, remove `id` here to make it work for recover
|
||||
observed_pure = [msg.dict(exclude={"id": True}) for msg in observed]
|
||||
existed_pure = [msg.dict(exclude={"id": True}) for msg in existed]
|
||||
for idx, new in enumerate(observed_pure):
|
||||
if new["cause_by"] in self._rc.watch and new not in existed_pure:
|
||||
news.append(observed[idx])
|
||||
return news
|
||||
|
||||
observed = self._rc.env.memory.get_by_actions(self._rc.watch)
|
||||
|
||||
self._rc.news = self._rc.memory.find_news(observed) # find news (previously unseen messages) from observed messages
|
||||
async def _observe(self, ignore_memory=False) -> int:
|
||||
"""Prepare new messages for processing from the message buffer and other sources."""
|
||||
# Read unprocessed messages from the msg buffer.
|
||||
news = self._rc.msg_buffer.pop_all()
|
||||
if self.recovered:
|
||||
news = [self.latest_observed_msg] if self.latest_observed_msg else []
|
||||
else:
|
||||
self.latest_observed_msg = news[-1] if len(news) > 0 else None # record the latest observed msg
|
||||
|
||||
for i in env_msgs:
|
||||
self.recv(i)
|
||||
# Store the read messages in your own memory to prevent duplicate processing.
|
||||
old_messages = [] if ignore_memory else self._rc.memory.get()
|
||||
self._rc.memory.add_batch(news)
|
||||
# Filter out messages of interest.
|
||||
self._rc.news = self._find_news(news, old_messages)
|
||||
|
||||
# Design Rules:
|
||||
# If you need to further categorize Message objects, you can do so using the Message.set_meta function.
|
||||
# msg_buffer is a receiving buffer, avoid adding message data and operations to msg_buffer.
|
||||
news_text = [f"{i.role}: {i.content[:20]}..." for i in self._rc.news]
|
||||
if news_text:
|
||||
logger.debug(f'{self._setting} observed: {news_text}')
|
||||
logger.debug(f"{self._setting} observed: {news_text}")
|
||||
return len(self._rc.news)
|
||||
|
||||
def _publish_message(self, msg):
|
||||
def publish_message(self, msg):
|
||||
"""If the role belongs to env, then the role's messages will be broadcast to env"""
|
||||
if not msg:
|
||||
return
|
||||
if not self._rc.env:
|
||||
# If env does not exist, do not publish the message
|
||||
return
|
||||
self._rc.env.publish_message(msg)
|
||||
|
||||
def put_message(self, message):
|
||||
"""Place the message into the Role object's private message buffer."""
|
||||
if not message:
|
||||
return
|
||||
self._rc.msg_buffer.push(message)
|
||||
|
||||
async def _react(self) -> Message:
|
||||
"""Think first, then act, until the Role _think it is time to stop and requires no more todo.
|
||||
This is the standard think-act loop in the ReAct paper, which alternates thinking and acting in task solving, i.e. _think -> _act -> _think -> _act -> ...
|
||||
This is the standard think-act loop in the ReAct paper, which alternates thinking and acting in task solving, i.e. _think -> _act -> _think -> _act -> ...
|
||||
Use llm to select actions in _think dynamically
|
||||
"""
|
||||
actions_taken = 0
|
||||
rsp = Message("No actions taken yet") # will be overwritten after Role _act
|
||||
rsp = Message(content="No actions taken yet") # will be overwritten after Role _act
|
||||
while actions_taken < self._rc.max_react_loop:
|
||||
# think
|
||||
await self._think()
|
||||
|
|
@ -261,21 +456,22 @@ class Role:
|
|||
break
|
||||
# act
|
||||
logger.debug(f"{self._setting}: {self._rc.state=}, will do {self._rc.todo}")
|
||||
rsp = await self._act()
|
||||
rsp = await self._act() # 这个rsp是否需要publish_message?
|
||||
actions_taken += 1
|
||||
return rsp # return output from the last action
|
||||
return rsp # return output from the last action
|
||||
|
||||
async def _act_by_order(self) -> Message:
|
||||
"""switch action each time by order defined in _init_actions, i.e. _act (Action1) -> _act (Action2) -> ..."""
|
||||
for i in range(len(self._states)):
|
||||
start_idx = self._rc.state if self._rc.state >= 0 else 0 # action to run from recovered state
|
||||
for i in range(start_idx, len(self._states)):
|
||||
self._set_state(i)
|
||||
rsp = await self._act()
|
||||
return rsp # return output from the last action
|
||||
return rsp # return output from the last action
|
||||
|
||||
async def _plan_and_act(self) -> Message:
|
||||
"""first plan, then execute an action sequence, i.e. _think (of a plan) -> _act -> _act -> ... Use llm to come up with the plan dynamically."""
|
||||
# TODO: to be implemented
|
||||
return Message("")
|
||||
return Message(content="")
|
||||
|
||||
async def react(self) -> Message:
|
||||
"""Entry to one of three strategies by which Role reacts to the observed Message"""
|
||||
|
|
@ -285,43 +481,59 @@ class Role:
|
|||
rsp = await self._act_by_order()
|
||||
elif self._rc.react_mode == RoleReactMode.PLAN_AND_ACT:
|
||||
rsp = await self._plan_and_act()
|
||||
self._set_state(state=-1) # current reaction is complete, reset state to -1 and todo back to None
|
||||
self._set_state(state=-1) # current reaction is complete, reset state to -1 and todo back to None
|
||||
return rsp
|
||||
|
||||
def recv(self, message: Message) -> None:
|
||||
"""add message to history."""
|
||||
# self._history += f"\n{message}"
|
||||
# self._context = self._history
|
||||
if message in self._rc.memory.get():
|
||||
return
|
||||
self._rc.memory.add(message)
|
||||
# # Replaced by run()
|
||||
# def recv(self, message: Message) -> None:
|
||||
# """add message to history."""
|
||||
# # self._history += f"\n{message}"
|
||||
# # self._context = self._history
|
||||
# if message in self._rc.memory.get():
|
||||
# return
|
||||
# self._rc.memory.add(message)
|
||||
|
||||
async def handle(self, message: Message) -> Message:
|
||||
"""Receive information and reply with actions"""
|
||||
# logger.debug(f"{self.name=}, {self.profile=}, {message.role=}")
|
||||
self.recv(message)
|
||||
|
||||
return await self._react()
|
||||
# # Replaced by run()
|
||||
# async def handle(self, message: Message) -> Message:
|
||||
# """Receive information and reply with actions"""
|
||||
# # logger.debug(f"{self.name=}, {self.profile=}, {message.role=}")
|
||||
# self.recv(message)
|
||||
#
|
||||
# return await self._react()
|
||||
|
||||
def get_memories(self, k=0) -> list[Message]:
|
||||
"""A wrapper to return the most recent k memories of this role, return all when k=0"""
|
||||
return self._rc.memory.get(k=k)
|
||||
|
||||
async def run(self, message=None):
|
||||
@role_raise_decorator
|
||||
async def run(self, with_message=None):
|
||||
"""Observe, and think and act based on the results of the observation"""
|
||||
if message:
|
||||
if isinstance(message, str):
|
||||
message = Message(message)
|
||||
if isinstance(message, Message):
|
||||
self.recv(message)
|
||||
if isinstance(message, list):
|
||||
self.recv(Message("\n".join(message)))
|
||||
elif not await self._observe():
|
||||
if with_message:
|
||||
msg = None
|
||||
if isinstance(with_message, str):
|
||||
msg = Message(content=with_message)
|
||||
elif isinstance(with_message, Message):
|
||||
msg = with_message
|
||||
elif isinstance(with_message, list):
|
||||
msg = Message(content="\n".join(with_message))
|
||||
if not msg.cause_by:
|
||||
msg.cause_by = UserRequirement
|
||||
self.put_message(msg)
|
||||
|
||||
if not await self._observe():
|
||||
# If there is no new information, suspend and wait
|
||||
logger.debug(f"{self._setting}: no news. waiting.")
|
||||
return
|
||||
|
||||
rsp = await self.react()
|
||||
# Publish the reply to the environment, waiting for the next subscriber to process
|
||||
self._publish_message(rsp)
|
||||
|
||||
# Reset the next action to be taken.
|
||||
self._rc.todo = None
|
||||
# Send the response message to the Environment object to have it relay the message to the subscribers.
|
||||
self.publish_message(rsp)
|
||||
return rsp
|
||||
|
||||
@property
|
||||
def is_idle(self) -> bool:
|
||||
"""If true, all actions have been executed."""
|
||||
return not self._rc.news and not self._rc.todo and self._rc.msg_buffer.empty()
|
||||
|
|
|
|||
|
|
@ -5,31 +5,33 @@
|
|||
@Author : alexanderwu
|
||||
@File : sales.py
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from metagpt.actions import SearchAndSummarize
|
||||
from metagpt.roles import Role
|
||||
from metagpt.tools import SearchEngineType
|
||||
|
||||
|
||||
class Sales(Role):
|
||||
def __init__(
|
||||
self,
|
||||
name="Xiaomei",
|
||||
profile="Retail sales guide",
|
||||
desc="I am a sales guide in retail. My name is Xiaomei. I will answer some customer questions next, and I "
|
||||
"will answer questions only based on the information in the knowledge base."
|
||||
"If I feel that you can't get the answer from the reference material, then I will directly reply that"
|
||||
" I don't know, and I won't tell you that this is from the knowledge base,"
|
||||
"but pretend to be what I know. Note that each of my replies will be replied in the tone of a "
|
||||
"professional guide",
|
||||
store=None
|
||||
):
|
||||
super().__init__(name, profile, desc=desc)
|
||||
self._set_store(store)
|
||||
name: str = "Xiaomei"
|
||||
profile: str = "Retail sales guide"
|
||||
desc: str = "I am a sales guide in retail. My name is Xiaomei. I will answer some customer questions next, and I "
|
||||
"will answer questions only based on the information in the knowledge base."
|
||||
"If I feel that you can't get the answer from the reference material, then I will directly reply that"
|
||||
" I don't know, and I won't tell you that this is from the knowledge base,"
|
||||
"but pretend to be what I know. Note that each of my replies will be replied in the tone of a "
|
||||
"professional guide"
|
||||
|
||||
store: Optional[str] = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._set_store(self.store)
|
||||
|
||||
def _set_store(self, store):
|
||||
if store:
|
||||
action = SearchAndSummarize("", engine=SearchEngineType.CUSTOM_ENGINE, search_func=store.search)
|
||||
action = SearchAndSummarize("", engine=SearchEngineType.CUSTOM_ENGINE, search_func=store.asearch)
|
||||
else:
|
||||
action = SearchAndSummarize()
|
||||
self._init_actions([action])
|
||||
|
||||
|
|
@ -3,9 +3,15 @@
|
|||
"""
|
||||
@Time : 2023/5/23 17:25
|
||||
@Author : alexanderwu
|
||||
@File : seacher.py
|
||||
@File : searcher.py
|
||||
@Modified By: mashenquan, 2023-11-1. According to Chapter 2.2.1 and 2.2.2 of RFC 116, change the data type of
|
||||
the `cause_by` value in the `Message` to a string to support the new message distribution feature.
|
||||
"""
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from metagpt.actions import ActionOutput, SearchAndSummarize
|
||||
from metagpt.actions.action_node import ActionNode
|
||||
from metagpt.logs import logger
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import Message
|
||||
|
|
@ -15,7 +21,7 @@ from metagpt.tools import SearchEngineType
|
|||
class Searcher(Role):
|
||||
"""
|
||||
Represents a Searcher role responsible for providing search services to users.
|
||||
|
||||
|
||||
Attributes:
|
||||
name (str): Name of the searcher.
|
||||
profile (str): Role profile.
|
||||
|
|
@ -23,17 +29,17 @@ class Searcher(Role):
|
|||
constraints (str): Constraints or limitations for the searcher.
|
||||
engine (SearchEngineType): The type of search engine to use.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
name: str = 'Alice',
|
||||
profile: str = 'Smart Assistant',
|
||||
goal: str = 'Provide search services for users',
|
||||
constraints: str = 'Answer is rich and complete',
|
||||
engine=SearchEngineType.SERPAPI_GOOGLE,
|
||||
**kwargs) -> None:
|
||||
|
||||
name: str = Field(default="Alice")
|
||||
profile: str = Field(default="Smart Assistant")
|
||||
goal: str = "Provide search services for users"
|
||||
constraints: str = "Answer is rich and complete"
|
||||
engine: SearchEngineType = SearchEngineType.SERPAPI_GOOGLE
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
"""
|
||||
Initializes the Searcher role with given attributes.
|
||||
|
||||
|
||||
Args:
|
||||
name (str): Name of the searcher.
|
||||
profile (str): Role profile.
|
||||
|
|
@ -41,8 +47,8 @@ class Searcher(Role):
|
|||
constraints (str): Constraints or limitations for the searcher.
|
||||
engine (SearchEngineType): The type of search engine to use.
|
||||
"""
|
||||
super().__init__(name, profile, goal, constraints, **kwargs)
|
||||
self._init_actions([SearchAndSummarize(engine=engine)])
|
||||
super().__init__(**kwargs)
|
||||
self._init_actions([SearchAndSummarize(engine=self.engine)])
|
||||
|
||||
def set_search_func(self, search_func):
|
||||
"""Sets a custom search function for the searcher."""
|
||||
|
|
@ -53,12 +59,16 @@ class Searcher(Role):
|
|||
"""Performs the search action in a single process."""
|
||||
logger.info(f"{self._setting}: ready to {self._rc.todo}")
|
||||
response = await self._rc.todo.run(self._rc.memory.get(k=0))
|
||||
|
||||
if isinstance(response, ActionOutput):
|
||||
msg = Message(content=response.content, instruct_content=response.instruct_content,
|
||||
role=self.profile, cause_by=type(self._rc.todo))
|
||||
|
||||
if isinstance(response, (ActionOutput, ActionNode)):
|
||||
msg = Message(
|
||||
content=response.content,
|
||||
instruct_content=response.instruct_content,
|
||||
role=self.profile,
|
||||
cause_by=self._rc.todo,
|
||||
)
|
||||
else:
|
||||
msg = Message(content=response, role=self.profile, cause_by=type(self._rc.todo))
|
||||
msg = Message(content=response, role=self.profile, cause_by=self._rc.todo)
|
||||
self._rc.memory.add(msg)
|
||||
return msg
|
||||
|
||||
|
|
@ -4,12 +4,14 @@
|
|||
@Time : 2023/9/13 12:23
|
||||
@Author : femto Zheng
|
||||
@File : sk_agent.py
|
||||
@Modified By: mashenquan, 2023-11-1. In accordance with Chapter 2.2.1 and 2.2.2 of RFC 116, utilize the new message
|
||||
distribution feature for message filtering.
|
||||
"""
|
||||
from semantic_kernel.planning import SequentialPlanner
|
||||
from semantic_kernel.planning.action_planner.action_planner import ActionPlanner
|
||||
from semantic_kernel.planning.basic_planner import BasicPlanner
|
||||
|
||||
from metagpt.actions import BossRequirement
|
||||
from metagpt.actions import UserRequirement
|
||||
from metagpt.actions.execute_task import ExecuteTask
|
||||
from metagpt.logs import logger
|
||||
from metagpt.roles import Role
|
||||
|
|
@ -39,7 +41,7 @@ class SkAgent(Role):
|
|||
"""Initializes the Engineer role with given attributes."""
|
||||
super().__init__(name, profile, goal, constraints)
|
||||
self._init_actions([ExecuteTask()])
|
||||
self._watch([BossRequirement])
|
||||
self._watch([UserRequirement])
|
||||
self.kernel = make_sk_kernel()
|
||||
|
||||
# how funny the interface is inconsistent
|
||||
|
|
@ -70,7 +72,6 @@ class SkAgent(Role):
|
|||
result = (await self.plan.invoke_async()).result
|
||||
logger.info(result)
|
||||
|
||||
msg = Message(content=result, role=self.profile, cause_by=type(self._rc.todo))
|
||||
msg = Message(content=result, role=self.profile, cause_by=self._rc.todo)
|
||||
self._rc.memory.add(msg)
|
||||
# logger.debug(f"{response}")
|
||||
return msg
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
from datetime import datetime
|
||||
from typing import Dict
|
||||
|
||||
from metagpt.actions.write_tutorial import WriteDirectory, WriteContent
|
||||
from metagpt.actions.write_tutorial import WriteContent, WriteDirectory
|
||||
from metagpt.const import TUTORIAL_PATH
|
||||
from metagpt.logs import logger
|
||||
from metagpt.roles import Role
|
||||
|
|
@ -42,17 +42,7 @@ class TutorialAssistant(Role):
|
|||
self.main_title = ""
|
||||
self.total_content = ""
|
||||
self.language = language
|
||||
|
||||
async def _think(self) -> None:
|
||||
"""Determine the next action to be taken by the role."""
|
||||
if self._rc.todo is None:
|
||||
self._set_state(0)
|
||||
return
|
||||
|
||||
if self._rc.state + 1 < len(self._states):
|
||||
self._set_state(self._rc.state + 1)
|
||||
else:
|
||||
self._rc.todo = None
|
||||
self._set_react_mode(react_mode="by_order")
|
||||
|
||||
async def _handle_directory(self, titles: Dict) -> Message:
|
||||
"""Handle the directories for the tutorial document.
|
||||
|
|
@ -75,8 +65,6 @@ class TutorialAssistant(Role):
|
|||
for second_dir in first_dir[key]:
|
||||
directory += f" - {second_dir}\n"
|
||||
self._init_actions(actions)
|
||||
self._rc.todo = None
|
||||
return Message(content=directory)
|
||||
|
||||
async def _act(self) -> Message:
|
||||
"""Perform an action as determined by the role.
|
||||
|
|
@ -90,7 +78,8 @@ class TutorialAssistant(Role):
|
|||
self.topic = msg.content
|
||||
resp = await todo.run(topic=self.topic)
|
||||
logger.info(resp)
|
||||
return await self._handle_directory(resp)
|
||||
await self._handle_directory(resp)
|
||||
return await super().react()
|
||||
resp = await todo.run(topic=self.topic)
|
||||
logger.info(resp)
|
||||
if self.total_content != "":
|
||||
|
|
@ -98,17 +87,8 @@ class TutorialAssistant(Role):
|
|||
self.total_content += resp
|
||||
return Message(content=resp, role=self.profile)
|
||||
|
||||
async def _react(self) -> Message:
|
||||
"""Execute the assistant's think and actions.
|
||||
|
||||
Returns:
|
||||
A message containing the final result of the assistant's actions.
|
||||
"""
|
||||
while True:
|
||||
await self._think()
|
||||
if self._rc.todo is None:
|
||||
break
|
||||
msg = await self._act()
|
||||
async def react(self) -> Message:
|
||||
msg = await super().react()
|
||||
root_path = TUTORIAL_PATH / datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
await File.write(root_path, f"{self.main_title}.md", self.total_content.encode('utf-8'))
|
||||
await File.write(root_path, f"{self.main_title}.md", self.total_content.encode("utf-8"))
|
||||
return msg
|
||||
|
|
|
|||
|
|
@ -4,15 +4,46 @@
|
|||
@Time : 2023/5/8 22:12
|
||||
@Author : alexanderwu
|
||||
@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.
|
||||
1. Add `Document` and `Documents` for `FileRepository` in Section 2.2.3.4 of RFC 135.
|
||||
2. Encapsulate the common key-values set to pydantic structures to standardize and unify parameter passing
|
||||
between actions.
|
||||
3. Add `id` to `Message` according to Section 2.2.3.1.1 of RFC 135.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Type, TypedDict
|
||||
import asyncio
|
||||
import json
|
||||
import os.path
|
||||
import uuid
|
||||
from abc import ABC
|
||||
from asyncio import Queue, QueueEmpty, wait_for
|
||||
from json import JSONDecodeError
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Set, Type, TypedDict, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.const import (
|
||||
MESSAGE_ROUTE_CAUSE_BY,
|
||||
MESSAGE_ROUTE_FROM,
|
||||
MESSAGE_ROUTE_TO,
|
||||
MESSAGE_ROUTE_TO_ALL,
|
||||
SYSTEM_DESIGN_FILE_REPO,
|
||||
TASK_FILE_REPO,
|
||||
)
|
||||
from metagpt.logs import logger
|
||||
from metagpt.utils.common import any_to_str, any_to_str_set, import_class
|
||||
from metagpt.utils.exceptions import handle_exception
|
||||
from metagpt.utils.serialize import (
|
||||
actionoutout_schema_to_mapping,
|
||||
actionoutput_mapping_to_str,
|
||||
actionoutput_str_to_mapping,
|
||||
)
|
||||
|
||||
|
||||
class RawMessage(TypedDict):
|
||||
|
|
@ -20,16 +51,111 @@ class RawMessage(TypedDict):
|
|||
role: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Message:
|
||||
class Document(BaseModel):
|
||||
"""
|
||||
Represents a document.
|
||||
"""
|
||||
|
||||
root_path: str = ""
|
||||
filename: str = ""
|
||||
content: str = ""
|
||||
|
||||
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)
|
||||
|
||||
@property
|
||||
def full_path(self):
|
||||
if not CONFIG.git_repo:
|
||||
return None
|
||||
return str(CONFIG.git_repo.workdir / self.root_path / self.filename)
|
||||
|
||||
def __str__(self):
|
||||
return self.content
|
||||
|
||||
def __repr__(self):
|
||||
return self.content
|
||||
|
||||
|
||||
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>]"""
|
||||
|
||||
id: str # According to Section 2.2.3.1.1 of RFC 135
|
||||
content: str
|
||||
instruct_content: BaseModel = field(default=None)
|
||||
role: str = field(default='user') # system / user / assistant
|
||||
cause_by: Type["Action"] = field(default="")
|
||||
sent_from: str = field(default="")
|
||||
send_to: str = field(default="")
|
||||
restricted_to: str = field(default="")
|
||||
instruct_content: BaseModel = None
|
||||
role: str = "user" # system / user / assistant
|
||||
cause_by: str = ""
|
||||
sent_from: str = ""
|
||||
send_to: Set = Field(default_factory={MESSAGE_ROUTE_TO_ALL})
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
ic = kwargs.get("instruct_content", None)
|
||||
if ic and not isinstance(ic, BaseModel) and "class" in ic:
|
||||
# compatible with custom-defined ActionOutput
|
||||
mapping = actionoutput_str_to_mapping(ic["mapping"])
|
||||
|
||||
actionnode_class = import_class("ActionNode", "metagpt.actions.action_node") # avoid circular import
|
||||
ic_obj = actionnode_class.create_model_class(class_name=ic["class"], mapping=mapping)
|
||||
ic_new = ic_obj(**ic["value"])
|
||||
kwargs["instruct_content"] = ic_new
|
||||
|
||||
kwargs["id"] = kwargs.get("id", uuid.uuid4().hex)
|
||||
kwargs["cause_by"] = any_to_str(
|
||||
kwargs.get("cause_by", import_class("UserRequirement", "metagpt.actions.add_requirement"))
|
||||
)
|
||||
kwargs["sent_from"] = any_to_str(kwargs.get("sent_from", ""))
|
||||
kwargs["send_to"] = any_to_str_set(kwargs.get("send_to", {MESSAGE_ROUTE_TO_ALL}))
|
||||
super(Message, self).__init__(**kwargs)
|
||||
|
||||
def __setattr__(self, key, val):
|
||||
"""Override `@property.setter`, convert non-string parameters into string parameters."""
|
||||
if key == MESSAGE_ROUTE_CAUSE_BY:
|
||||
new_val = any_to_str(val)
|
||||
elif key == MESSAGE_ROUTE_FROM:
|
||||
new_val = any_to_str(val)
|
||||
elif key == MESSAGE_ROUTE_TO:
|
||||
new_val = any_to_str_set(val)
|
||||
else:
|
||||
new_val = val
|
||||
super().__setattr__(key, new_val)
|
||||
|
||||
def dict(self, *args, **kwargs) -> "DictStrAny":
|
||||
"""overwrite the `dict` to dump dynamic pydantic model"""
|
||||
obj_dict = super(Message, self).dict(*args, **kwargs)
|
||||
ic = self.instruct_content
|
||||
if ic:
|
||||
# compatible with custom-defined ActionOutput
|
||||
schema = ic.schema()
|
||||
# `Documents` contain definitions
|
||||
if "definitions" not in schema:
|
||||
# TODO refine with nested BaseModel
|
||||
mapping = actionoutout_schema_to_mapping(schema)
|
||||
mapping = actionoutput_mapping_to_str(mapping)
|
||||
|
||||
obj_dict["instruct_content"] = {"class": schema["title"], "mapping": mapping, "value": ic.dict()}
|
||||
return obj_dict
|
||||
|
||||
def __str__(self):
|
||||
# prefix = '-'.join([self.role, str(self.cause_by)])
|
||||
|
|
@ -39,45 +165,190 @@ class Message:
|
|||
return self.__str__()
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"role": self.role,
|
||||
"content": self.content
|
||||
}
|
||||
"""Return a dict containing `role` and `content` for the LLM call.l"""
|
||||
return {"role": self.role, "content": self.content}
|
||||
|
||||
def dump(self) -> str:
|
||||
"""Convert the object to json string"""
|
||||
return self.json(exclude_none=True)
|
||||
|
||||
@staticmethod
|
||||
@handle_exception(exception_type=JSONDecodeError, default_return=None)
|
||||
def load(val):
|
||||
"""Convert the json string to object."""
|
||||
i = json.loads(val)
|
||||
return Message(**i)
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserMessage(Message):
|
||||
"""便于支持OpenAI的消息
|
||||
Facilitate support for OpenAI messages
|
||||
Facilitate support for OpenAI messages
|
||||
"""
|
||||
|
||||
def __init__(self, content: str):
|
||||
super().__init__(content, 'user')
|
||||
super().__init__(content=content, role="user")
|
||||
|
||||
|
||||
@dataclass
|
||||
class SystemMessage(Message):
|
||||
"""便于支持OpenAI的消息
|
||||
Facilitate support for OpenAI messages
|
||||
Facilitate support for OpenAI messages
|
||||
"""
|
||||
|
||||
def __init__(self, content: str):
|
||||
super().__init__(content, 'system')
|
||||
super().__init__(content=content, role="system")
|
||||
|
||||
|
||||
@dataclass
|
||||
class AIMessage(Message):
|
||||
"""便于支持OpenAI的消息
|
||||
Facilitate support for OpenAI messages
|
||||
Facilitate support for OpenAI messages
|
||||
"""
|
||||
|
||||
def __init__(self, content: str):
|
||||
super().__init__(content, 'assistant')
|
||||
super().__init__(content=content, role="assistant")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_content = 'test_message'
|
||||
msgs = [
|
||||
UserMessage(test_content),
|
||||
SystemMessage(test_content),
|
||||
AIMessage(test_content),
|
||||
Message(test_content, role='QA')
|
||||
]
|
||||
logger.info(msgs)
|
||||
class MessageQueue(BaseModel):
|
||||
"""Message queue which supports asynchronous updates."""
|
||||
|
||||
_queue: Queue = Field(default_factory=Queue)
|
||||
|
||||
_private_attributes = {"_queue": Queue()}
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def __init__(self, **kwargs: Any):
|
||||
for key in self._private_attributes.keys():
|
||||
if key in kwargs:
|
||||
object.__setattr__(self, key, kwargs[key])
|
||||
else:
|
||||
object.__setattr__(self, key, Queue())
|
||||
|
||||
def pop(self) -> Message | None:
|
||||
"""Pop one message from the queue."""
|
||||
try:
|
||||
item = self._queue.get_nowait()
|
||||
if item:
|
||||
self._queue.task_done()
|
||||
return item
|
||||
except QueueEmpty:
|
||||
return None
|
||||
|
||||
def pop_all(self) -> List[Message]:
|
||||
"""Pop all messages from the queue."""
|
||||
ret = []
|
||||
while True:
|
||||
msg = self.pop()
|
||||
if not msg:
|
||||
break
|
||||
ret.append(msg)
|
||||
return ret
|
||||
|
||||
def push(self, msg: Message):
|
||||
"""Push a message into the queue."""
|
||||
self._queue.put_nowait(msg)
|
||||
|
||||
def empty(self):
|
||||
"""Return true if the queue is empty."""
|
||||
return self._queue.empty()
|
||||
|
||||
async def dump(self) -> str:
|
||||
"""Convert the `MessageQueue` object to a json string."""
|
||||
if self.empty():
|
||||
return "[]"
|
||||
|
||||
lst = []
|
||||
try:
|
||||
while True:
|
||||
item = await wait_for(self._queue.get(), timeout=1.0)
|
||||
if item is None:
|
||||
break
|
||||
lst.append(item.dict(exclude_none=True))
|
||||
self._queue.task_done()
|
||||
except asyncio.TimeoutError:
|
||||
logger.debug("Queue is empty, exiting...")
|
||||
return json.dumps(lst)
|
||||
|
||||
@staticmethod
|
||||
def load(data) -> "MessageQueue":
|
||||
"""Convert the json string to the `MessageQueue` object."""
|
||||
queue = MessageQueue()
|
||||
try:
|
||||
lst = json.loads(data)
|
||||
for i in lst:
|
||||
msg = Message(**i)
|
||||
queue.push(msg)
|
||||
except JSONDecodeError as e:
|
||||
logger.warning(f"JSON load failed: {data}, error:{e}")
|
||||
|
||||
return queue
|
||||
|
||||
|
||||
# 定义一个泛型类型变量
|
||||
T = TypeVar("T", bound="BaseModel")
|
||||
|
||||
|
||||
class BaseContext(BaseModel, ABC):
|
||||
@classmethod
|
||||
@handle_exception
|
||||
def loads(cls: Type[T], val: str) -> Optional[T]:
|
||||
i = json.loads(val)
|
||||
return cls(**i)
|
||||
|
||||
|
||||
class CodingContext(BaseContext):
|
||||
filename: str
|
||||
design_doc: Optional[Document]
|
||||
task_doc: Optional[Document]
|
||||
code_doc: Optional[Document]
|
||||
|
||||
|
||||
class TestingContext(BaseContext):
|
||||
filename: str
|
||||
code_doc: Document
|
||||
test_doc: Optional[Document]
|
||||
|
||||
|
||||
class RunCodeContext(BaseContext):
|
||||
mode: str = "script"
|
||||
code: Optional[str]
|
||||
code_filename: str = ""
|
||||
test_code: Optional[str]
|
||||
test_filename: str = ""
|
||||
command: List[str] = Field(default_factory=list)
|
||||
working_directory: str = ""
|
||||
additional_python_paths: List[str] = Field(default_factory=list)
|
||||
output_filename: Optional[str]
|
||||
output: Optional[str]
|
||||
|
||||
|
||||
class RunCodeResult(BaseContext):
|
||||
summary: str
|
||||
stdout: str
|
||||
stderr: str
|
||||
|
||||
|
||||
class CodeSummarizeContext(BaseModel):
|
||||
design_filename: str = ""
|
||||
task_filename: str = ""
|
||||
codes_filenames: List[str] = Field(default_factory=list)
|
||||
reason: str = ""
|
||||
|
||||
@staticmethod
|
||||
def loads(filenames: List) -> CodeSummarizeContext:
|
||||
ctx = CodeSummarizeContext()
|
||||
for filename in filenames:
|
||||
if Path(filename).is_relative_to(SYSTEM_DESIGN_FILE_REPO):
|
||||
ctx.design_filename = str(filename)
|
||||
continue
|
||||
if Path(filename).is_relative_to(TASK_FILE_REPO):
|
||||
ctx.task_filename = str(filename)
|
||||
continue
|
||||
return ctx
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.design_filename, self.task_filename))
|
||||
|
||||
|
||||
class BugFixContext(BaseContext):
|
||||
filename: str = ""
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/5/12 00:30
|
||||
@Author : alexanderwu
|
||||
@File : software_company.py
|
||||
"""
|
||||
from metagpt.team import Team as SoftwareCompany
|
||||
|
||||
import warnings
|
||||
warnings.warn("metagpt.software_company is deprecated and will be removed in the future"
|
||||
"Please use metagpt.team instead. SoftwareCompany class is now named as Team.",
|
||||
DeprecationWarning, 2)
|
||||
79
metagpt/startup.py
Normal file
79
metagpt/startup.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
|
||||
from metagpt.config import CONFIG
|
||||
|
||||
app = typer.Typer(add_completion=False)
|
||||
|
||||
|
||||
@app.command()
|
||||
def startup(
|
||||
idea: str = typer.Argument(..., help="Your innovative idea, such as 'Create a 2048 game.'"),
|
||||
investment: float = typer.Option(default=3.0, help="Dollar amount to invest in the AI company."),
|
||||
n_round: int = typer.Option(default=5, help="Number of rounds for the simulation."),
|
||||
code_review: bool = typer.Option(default=True, help="Whether to use code review."),
|
||||
run_tests: bool = typer.Option(default=False, help="Whether to enable QA for adding & running tests."),
|
||||
implement: bool = typer.Option(default=True, help="Enable or disable code implementation."),
|
||||
project_name: str = typer.Option(default="", help="Unique project name, such as 'game_2048'."),
|
||||
inc: bool = typer.Option(default=False, help="Incremental mode. Use it to coop with existing repo."),
|
||||
project_path: str = typer.Option(
|
||||
default="",
|
||||
help="Specify the directory path of the old version project to fulfill the incremental requirements.",
|
||||
),
|
||||
reqa_file: str = typer.Option(
|
||||
default="", help="Specify the source file name for rewriting the quality assurance code."
|
||||
),
|
||||
max_auto_summarize_code: int = typer.Option(
|
||||
default=0,
|
||||
help="The maximum number of times the 'SummarizeCode' action is automatically invoked, with -1 indicating "
|
||||
"unlimited. This parameter is used for debugging the workflow.",
|
||||
),
|
||||
recover_path: str = typer.Option(default=None, help="recover the project from existing serialized storage"),
|
||||
):
|
||||
"""Run a startup. Be a boss."""
|
||||
from metagpt.roles import (
|
||||
Architect,
|
||||
Engineer,
|
||||
ProductManager,
|
||||
ProjectManager,
|
||||
QaEngineer,
|
||||
)
|
||||
from metagpt.team import Team
|
||||
|
||||
CONFIG.update_via_cli(project_path, project_name, inc, reqa_file, max_auto_summarize_code)
|
||||
|
||||
if not recover_path:
|
||||
company = Team()
|
||||
company.hire(
|
||||
[
|
||||
ProductManager(),
|
||||
Architect(),
|
||||
ProjectManager(),
|
||||
]
|
||||
)
|
||||
|
||||
if implement or code_review:
|
||||
company.hire([Engineer(n_borg=5, use_code_review=code_review)])
|
||||
|
||||
if run_tests:
|
||||
company.hire([QaEngineer()])
|
||||
else:
|
||||
# # stg_path = SERDESER_PATH.joinpath("team")
|
||||
stg_path = Path(recover_path)
|
||||
if not stg_path.exists() or not str(stg_path).endswith("team"):
|
||||
raise FileNotFoundError(f"{recover_path} not exists or not endswith `team`")
|
||||
|
||||
company = Team.deserialize(stg_path=stg_path)
|
||||
idea = company.idea # use original idea
|
||||
|
||||
company.invest(investment)
|
||||
company.run_project(idea)
|
||||
asyncio.run(company.run(n_round=n_round))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
101
metagpt/subscription.py
Normal file
101
metagpt/subscription.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import asyncio
|
||||
from typing import AsyncGenerator, Awaitable, Callable
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from metagpt.logs import logger
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import Message
|
||||
|
||||
|
||||
class SubscriptionRunner(BaseModel):
|
||||
"""A simple wrapper to manage subscription tasks for different roles using asyncio.
|
||||
|
||||
Example:
|
||||
>>> import asyncio
|
||||
>>> from metagpt.subscription import SubscriptionRunner
|
||||
>>> from metagpt.roles import Searcher
|
||||
>>> from metagpt.schema import Message
|
||||
|
||||
>>> async def trigger():
|
||||
... while True:
|
||||
... yield Message("the latest news about OpenAI")
|
||||
... await asyncio.sleep(3600 * 24)
|
||||
|
||||
>>> async def callback(msg: Message):
|
||||
... print(msg.content)
|
||||
|
||||
>>> async def main():
|
||||
... pb = SubscriptionRunner()
|
||||
... await pb.subscribe(Searcher(), trigger(), callback)
|
||||
... await pb.run()
|
||||
|
||||
>>> asyncio.run(main())
|
||||
"""
|
||||
|
||||
tasks: dict[Role, asyncio.Task] = Field(default_factory=dict)
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
async def subscribe(
|
||||
self,
|
||||
role: Role,
|
||||
trigger: AsyncGenerator[Message, None],
|
||||
callback: Callable[
|
||||
[
|
||||
Message,
|
||||
],
|
||||
Awaitable[None],
|
||||
],
|
||||
):
|
||||
"""Subscribes a role to a trigger and sets up a callback to be called with the role's response.
|
||||
|
||||
Args:
|
||||
role: The role to subscribe.
|
||||
trigger: An asynchronous generator that yields Messages to be processed by the role.
|
||||
callback: An asynchronous function to be called with the response from the role.
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
async def _start_role():
|
||||
async for msg in trigger:
|
||||
resp = await role.run(msg)
|
||||
await callback(resp)
|
||||
|
||||
self.tasks[role] = loop.create_task(_start_role(), name=f"Subscription-{role}")
|
||||
|
||||
async def unsubscribe(self, role: Role):
|
||||
"""Unsubscribes a role from its trigger and cancels the associated task.
|
||||
|
||||
Args:
|
||||
role: The role to unsubscribe.
|
||||
"""
|
||||
task = self.tasks.pop(role)
|
||||
task.cancel()
|
||||
|
||||
async def run(self, raise_exception: bool = True):
|
||||
"""Runs all subscribed tasks and handles their completion or exception.
|
||||
|
||||
Args:
|
||||
raise_exception: _description_. Defaults to True.
|
||||
|
||||
Raises:
|
||||
task.exception: _description_
|
||||
"""
|
||||
while True:
|
||||
for role, task in self.tasks.items():
|
||||
if task.done():
|
||||
if task.exception():
|
||||
if raise_exception:
|
||||
raise task.exception()
|
||||
logger.opt(exception=task.exception()).error(f"Task {task.get_name()} run error")
|
||||
else:
|
||||
logger.warning(
|
||||
f"Task {task.get_name()} has completed. "
|
||||
"If this is unexpected behavior, please check the trigger function."
|
||||
)
|
||||
self.tasks.pop(role)
|
||||
break
|
||||
else:
|
||||
await asyncio.sleep(1)
|
||||
|
|
@ -3,60 +3,125 @@
|
|||
"""
|
||||
@Time : 2023/5/12 00:30
|
||||
@Author : alexanderwu
|
||||
@File : software_company.py
|
||||
@File : team.py
|
||||
@Modified By: mashenquan, 2023/11/27. Add an archiving operation after completing the project, as specified in
|
||||
Section 2.2.3.3 of RFC 135.
|
||||
"""
|
||||
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from metagpt.actions import BossRequirement
|
||||
from metagpt.actions import UserRequirement
|
||||
from metagpt.config import CONFIG
|
||||
from metagpt.const import MESSAGE_ROUTE_TO_ALL, SERDESER_PATH
|
||||
from metagpt.environment import Environment
|
||||
from metagpt.logs import logger
|
||||
from metagpt.roles import Role
|
||||
from metagpt.schema import Message
|
||||
from metagpt.utils.common import NoMoneyException
|
||||
from metagpt.utils.common import (
|
||||
NoMoneyException,
|
||||
read_json_file,
|
||||
serialize_decorator,
|
||||
write_json_file,
|
||||
)
|
||||
|
||||
|
||||
class Team(BaseModel):
|
||||
"""
|
||||
Team: Possesses one or more roles (agents), SOP (Standard Operating Procedures), and a platform for instant messaging,
|
||||
dedicated to perform any multi-agent activity, such as collaboratively writing executable code.
|
||||
Team: Possesses one or more roles (agents), SOP (Standard Operating Procedures), and a env for instant messaging,
|
||||
dedicated to env any multi-agent activity, such as collaboratively writing executable code.
|
||||
"""
|
||||
environment: Environment = Field(default_factory=Environment)
|
||||
|
||||
env: Environment = Field(default_factory=Environment)
|
||||
investment: float = Field(default=10.0)
|
||||
idea: str = Field(default="")
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def serialize(self, stg_path: Path = None):
|
||||
stg_path = SERDESER_PATH.joinpath("team") if stg_path is None else stg_path
|
||||
|
||||
team_info_path = stg_path.joinpath("team_info.json")
|
||||
write_json_file(team_info_path, self.dict(exclude={"env": True}))
|
||||
|
||||
self.env.serialize(stg_path.joinpath("environment")) # save environment alone
|
||||
|
||||
@classmethod
|
||||
def recover(cls, stg_path: Path) -> "Team":
|
||||
return cls.deserialize(stg_path)
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, stg_path: Path) -> "Team":
|
||||
"""stg_path = ./storage/team"""
|
||||
# recover team_info
|
||||
team_info_path = stg_path.joinpath("team_info.json")
|
||||
if not team_info_path.exists():
|
||||
raise FileNotFoundError(
|
||||
"recover storage meta file `team_info.json` not exist, "
|
||||
"not to recover and please start a new project."
|
||||
)
|
||||
|
||||
team_info: dict = read_json_file(team_info_path)
|
||||
|
||||
# recover environment
|
||||
environment = Environment.deserialize(stg_path=stg_path.joinpath("environment"))
|
||||
team_info.update({"env": environment})
|
||||
|
||||
team = Team(**team_info)
|
||||
return team
|
||||
|
||||
def hire(self, roles: list[Role]):
|
||||
"""Hire roles to cooperate"""
|
||||
self.environment.add_roles(roles)
|
||||
self.env.add_roles(roles)
|
||||
|
||||
def invest(self, investment: float):
|
||||
"""Invest company. raise NoMoneyException when exceed max_budget."""
|
||||
self.investment = investment
|
||||
CONFIG.max_budget = investment
|
||||
logger.info(f'Investment: ${investment}.')
|
||||
logger.info(f"Investment: ${investment}.")
|
||||
|
||||
def _check_balance(self):
|
||||
if CONFIG.total_cost > CONFIG.max_budget:
|
||||
raise NoMoneyException(CONFIG.total_cost, f'Insufficient funds: {CONFIG.max_budget}')
|
||||
raise NoMoneyException(CONFIG.total_cost, f"Insufficient funds: {CONFIG.max_budget}")
|
||||
|
||||
def run_project(self, idea, send_to: str = ""):
|
||||
"""Run a project from publishing user requirement."""
|
||||
self.idea = idea
|
||||
|
||||
# Human requirement.
|
||||
self.env.publish_message(
|
||||
Message(role="Human", content=idea, cause_by=UserRequirement, send_to=send_to or MESSAGE_ROUTE_TO_ALL)
|
||||
)
|
||||
|
||||
def start_project(self, idea, send_to: str = ""):
|
||||
"""Start a project from publishing boss requirement."""
|
||||
self.idea = idea
|
||||
self.environment.publish_message(Message(role="Human", content=idea, cause_by=BossRequirement, send_to=send_to))
|
||||
"""
|
||||
Deprecated: This method will be removed in the future.
|
||||
Please use the `run_project` method instead.
|
||||
"""
|
||||
warnings.warn(
|
||||
"The 'start_project' method is deprecated and will be removed in the future. "
|
||||
"Please use the 'run_project' method instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.run_project(idea=idea, send_to=send_to)
|
||||
|
||||
def _save(self):
|
||||
logger.info(self.json())
|
||||
logger.info(self.json(ensure_ascii=False))
|
||||
|
||||
@serialize_decorator
|
||||
async def run(self, n_round=3):
|
||||
"""Run company until target round or no money"""
|
||||
while n_round > 0:
|
||||
# self._save()
|
||||
n_round -= 1
|
||||
logger.debug(f"{n_round=}")
|
||||
logger.debug(f"max {n_round=} left.")
|
||||
self._check_balance()
|
||||
await self.environment.run()
|
||||
return self.environment.history
|
||||
|
||||
|
||||
await self.env.run()
|
||||
if CONFIG.git_repo:
|
||||
CONFIG.git_repo.archive()
|
||||
return self.env.history
|
||||
|
|
|
|||
42
metagpt/tools/azure_tts.py
Normal file
42
metagpt/tools/azure_tts.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
@Time : 2023/6/9 22:22
|
||||
@Author : Leo Xiao
|
||||
@File : azure_tts.py
|
||||
"""
|
||||
from azure.cognitiveservices.speech import AudioConfig, SpeechConfig, SpeechSynthesizer
|
||||
|
||||
from metagpt.config import CONFIG
|
||||
|
||||
|
||||
class AzureTTS:
|
||||
"""https://learn.microsoft.com/zh-cn/azure/cognitive-services/speech-service/language-support?tabs=tts#voice-styles-and-roles"""
|
||||
|
||||
@classmethod
|
||||
def synthesize_speech(cls, lang, voice, role, text, output_file):
|
||||
subscription_key = CONFIG.get("AZURE_TTS_SUBSCRIPTION_KEY")
|
||||
region = CONFIG.get("AZURE_TTS_REGION")
|
||||
speech_config = SpeechConfig(subscription=subscription_key, region=region)
|
||||
|
||||
speech_config.speech_synthesis_voice_name = voice
|
||||
audio_config = AudioConfig(filename=output_file)
|
||||
synthesizer = SpeechSynthesizer(speech_config=speech_config, audio_config=audio_config)
|
||||
|
||||
# if voice=="zh-CN-YunxiNeural":
|
||||
ssml_string = f"""
|
||||
<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='{lang}' xmlns:mstts='http://www.w3.org/2001/mstts'>
|
||||
<voice name='{voice}'>
|
||||
<mstts:express-as style='affectionate' role='{role}'>
|
||||
{text}
|
||||
</mstts:express-as>
|
||||
</voice>
|
||||
</speak>
|
||||
"""
|
||||
|
||||
synthesizer.speak_ssml_async(ssml_string).get()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
azure_tts = AzureTTS()
|
||||
azure_tts.synthesize_speech("zh-CN", "zh-CN-YunxiNeural", "Boy", "Hello, I am Kaka", "output.wav")
|
||||
|
|
@ -10,8 +10,9 @@ from typing import Union
|
|||
|
||||
class GPTPromptGenerator:
|
||||
"""Using LLM, given an output, request LLM to provide input (supporting instruction, chatbot, and query styles)"""
|
||||
|
||||
def __init__(self):
|
||||
self._generators = {i: getattr(self, f"gen_{i}_style") for i in ['instruction', 'chatbot', 'query']}
|
||||
self._generators = {i: getattr(self, f"gen_{i}_style") for i in ["instruction", "chatbot", "query"]}
|
||||
|
||||
def gen_instruction_style(self, example):
|
||||
"""Instruction style: Given an output, request LLM to provide input"""
|
||||
|
|
@ -35,7 +36,7 @@ Query: X
|
|||
Document: {example} What is the detailed query X?
|
||||
X:"""
|
||||
|
||||
def gen(self, example: str, style: str = 'all') -> Union[list[str], str]:
|
||||
def gen(self, example: str, style: str = "all") -> Union[list[str], str]:
|
||||
"""
|
||||
Generate one or multiple outputs using the example, allowing LLM to reply with the corresponding input
|
||||
|
||||
|
|
@ -43,7 +44,7 @@ X:"""
|
|||
:param style: (all|instruction|chatbot|query)
|
||||
:return: Expected LLM input sample (one or multiple)
|
||||
"""
|
||||
if style != 'all':
|
||||
if style != "all":
|
||||
return self._generators[style](example)
|
||||
return [f(example) for f in self._generators.values()]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# @Date : 2023/7/19 16:28
|
||||
# @Author : stellahong (stellahong@fuzhi.ai)
|
||||
# @Author : stellahong (stellahong@deepwisdom.ai)
|
||||
# @Desc :
|
||||
import asyncio
|
||||
import base64
|
||||
|
|
@ -13,11 +13,10 @@ from typing import List
|
|||
from aiohttp import ClientSession
|
||||
from PIL import Image, PngImagePlugin
|
||||
|
||||
from metagpt.config import Config
|
||||
from metagpt.const import WORKSPACE_ROOT
|
||||
from metagpt.logs import logger
|
||||
from metagpt.config import CONFIG
|
||||
|
||||
config = Config()
|
||||
# from metagpt.const import WORKSPACE_ROOT
|
||||
from metagpt.logs import logger
|
||||
|
||||
payload = {
|
||||
"prompt": "",
|
||||
|
|
@ -56,9 +55,8 @@ default_negative_prompt = "(easynegative:0.8),black, dark,Low resolution"
|
|||
class SDEngine:
|
||||
def __init__(self):
|
||||
# Initialize the SDEngine with configuration
|
||||
self.config = Config()
|
||||
self.sd_url = self.config.get("SD_URL")
|
||||
self.sd_t2i_url = f"{self.sd_url}{self.config.get('SD_T2I_API')}"
|
||||
self.sd_url = CONFIG.get("SD_URL")
|
||||
self.sd_t2i_url = f"{self.sd_url}{CONFIG.get('SD_T2I_API')}"
|
||||
# Define default payload settings for SD API
|
||||
self.payload = payload
|
||||
logger.info(self.sd_t2i_url)
|
||||
|
|
@ -81,7 +79,7 @@ class SDEngine:
|
|||
return self.payload
|
||||
|
||||
def _save(self, imgs, save_name=""):
|
||||
save_dir = WORKSPACE_ROOT / "resources" / "SD_Output"
|
||||
save_dir = CONFIG.workspace_path / "resources" / "SD_Output"
|
||||
if not os.path.exists(save_dir):
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
batch_decode_base64_to_image(imgs, save_dir, save_name=save_name)
|
||||
|
|
@ -120,11 +118,13 @@ def decode_base64_to_image(img, save_name):
|
|||
image.save(f"{save_name}.png", pnginfo=pnginfo)
|
||||
return pnginfo, image
|
||||
|
||||
|
||||
def batch_decode_base64_to_image(imgs, save_dir="", save_name=""):
|
||||
for idx, _img in enumerate(imgs):
|
||||
save_name = join(save_dir, save_name)
|
||||
decode_base64_to_image(_img, save_name=save_name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
engine = SDEngine()
|
||||
prompt = "pixel style, game design, a game interface should be minimalistic and intuitive with the score and high score displayed at the top. The snake and its food should be easily distinguishable. The game should have a simple color scheme, with a contrasting color for the snake and its food. Complete interface boundary"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
@File : search_engine.py
|
||||
"""
|
||||
import importlib
|
||||
from typing import Callable, Coroutine, Literal, overload, Optional, Union
|
||||
from typing import Callable, Coroutine, Literal, Optional, Union, overload
|
||||
|
||||
from semantic_kernel.skill_definition import sk_function
|
||||
|
||||
|
|
@ -43,8 +43,8 @@ class SearchEngine:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
engine: Optional[SearchEngineType] = None,
|
||||
run_func: Callable[[str, int, bool], Coroutine[None, None, Union[str, list[str]]]] = None,
|
||||
engine: Optional[SearchEngineType] = None,
|
||||
run_func: Callable[[str, int, bool], Coroutine[None, None, Union[str, list[str]]]] = None,
|
||||
):
|
||||
engine = engine or CONFIG.search_engine
|
||||
if engine == SearchEngineType.SERPAPI_GOOGLE:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ from typing import List
|
|||
import meilisearch
|
||||
from meilisearch.index import Index
|
||||
|
||||
from metagpt.utils.exceptions import handle_exception
|
||||
|
||||
|
||||
class DataSource:
|
||||
def __init__(self, name: str, url: str):
|
||||
|
|
@ -29,16 +31,12 @@ class MeilisearchEngine:
|
|||
def add_documents(self, data_source: DataSource, documents: List[dict]):
|
||||
index_name = f"{data_source.name}_index"
|
||||
if index_name not in self.client.get_indexes():
|
||||
self.client.create_index(uid=index_name, options={'primaryKey': 'id'})
|
||||
self.client.create_index(uid=index_name, options={"primaryKey": "id"})
|
||||
index = self.client.get_index(index_name)
|
||||
index.add_documents(documents)
|
||||
self.set_index(index)
|
||||
|
||||
@handle_exception(exception_type=Exception, default_return=[])
|
||||
def search(self, query):
|
||||
try:
|
||||
search_results = self._index.search(query)
|
||||
return search_results['hits']
|
||||
except Exception as e:
|
||||
# Handle MeiliSearch API errors
|
||||
print(f"MeiliSearch API error: {e}")
|
||||
return []
|
||||
search_results = self._index.search(query)
|
||||
return search_results["hits"]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
@File : translator.py
|
||||
"""
|
||||
|
||||
prompt = '''
|
||||
prompt = """
|
||||
# 指令
|
||||
接下来,作为一位拥有20年翻译经验的翻译专家,当我给出英文句子或段落时,你将提供通顺且具有可读性的{LANG}翻译。注意以下要求:
|
||||
1. 确保翻译结果流畅且易于理解
|
||||
|
|
@ -17,11 +17,10 @@ prompt = '''
|
|||
{ORIGINAL}
|
||||
|
||||
# 译文
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
class Translator:
|
||||
|
||||
@classmethod
|
||||
def translate_prompt(cls, original, lang='中文'):
|
||||
return prompt.format(LANG=lang, ORIGINAL=original)
|
||||
def translate_prompt(cls, original, lang="中文"):
|
||||
return prompt.format(LANG=lang, ORIGINAL=original)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from pathlib import Path
|
|||
|
||||
from metagpt.provider.openai_api import OpenAIGPTAPI as GPTAPI
|
||||
|
||||
ICL_SAMPLE = '''Interface definition:
|
||||
ICL_SAMPLE = """Interface definition:
|
||||
```text
|
||||
Interface Name: Element Tagging
|
||||
Interface Path: /projects/{project_key}/node-tags
|
||||
|
|
@ -60,20 +60,20 @@ def test_node_tags(project_key, nodes, operations, expected_msg):
|
|||
# 3. If comments are needed, use Chinese.
|
||||
|
||||
# If you understand, please wait for me to give the interface definition and just answer "Understood" to save tokens.
|
||||
'''
|
||||
"""
|
||||
|
||||
ACT_PROMPT_PREFIX = '''Refer to the test types: such as missing request parameters, field boundary verification, incorrect field type.
|
||||
ACT_PROMPT_PREFIX = """Refer to the test types: such as missing request parameters, field boundary verification, incorrect field type.
|
||||
Please output 10 test cases within one `@pytest.mark.parametrize` scope.
|
||||
```text
|
||||
'''
|
||||
"""
|
||||
|
||||
YFT_PROMPT_PREFIX = '''Refer to the test types: such as SQL injection, cross-site scripting (XSS), unauthorized access and privilege escalation,
|
||||
YFT_PROMPT_PREFIX = """Refer to the test types: such as SQL injection, cross-site scripting (XSS), unauthorized access and privilege escalation,
|
||||
authentication and authorization, parameter verification, exception handling, file upload and download.
|
||||
Please output 10 test cases within one `@pytest.mark.parametrize` scope.
|
||||
```text
|
||||
'''
|
||||
"""
|
||||
|
||||
OCR_API_DOC = '''```text
|
||||
OCR_API_DOC = """```text
|
||||
Interface Name: OCR recognition
|
||||
Interface Path: /api/v1/contract/treaty/task/ocr
|
||||
Method: POST
|
||||
|
|
@ -96,14 +96,20 @@ code integer Yes
|
|||
message string Yes
|
||||
data object Yes
|
||||
```
|
||||
'''
|
||||
"""
|
||||
|
||||
|
||||
class UTGenerator:
|
||||
"""UT Generator: Construct UT through API documentation"""
|
||||
|
||||
def __init__(self, swagger_file: str, ut_py_path: str, questions_path: str,
|
||||
chatgpt_method: str = "API", template_prefix=YFT_PROMPT_PREFIX) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
swagger_file: str,
|
||||
ut_py_path: str,
|
||||
questions_path: str,
|
||||
chatgpt_method: str = "API",
|
||||
template_prefix=YFT_PROMPT_PREFIX,
|
||||
) -> None:
|
||||
"""Initialize UT Generator
|
||||
|
||||
Args:
|
||||
|
|
@ -274,7 +280,7 @@ class UTGenerator:
|
|||
|
||||
def gpt_msgs_to_code(self, messages: list) -> str:
|
||||
"""Choose based on different calling methods"""
|
||||
result = ''
|
||||
result = ""
|
||||
if self.chatgpt_method == "API":
|
||||
result = GPTAPI().ask_code(msgs=messages)
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue