feat: merge geekan:main

This commit is contained in:
莘权 马 2023-12-22 16:40:04 +08:00
commit 9a1909bb95
139 changed files with 4649 additions and 1504 deletions

View file

@ -13,7 +13,6 @@ 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
@ -33,7 +32,6 @@ class ActionType(Enum):
WRITE_PRD_REVIEW = WritePRDReview
WRITE_DESIGN = WriteDesign
DESIGN_REVIEW = DesignReview
DESIGN_FILENAMES = DesignFilenames
WRTIE_CODE = WriteCode
WRITE_CODE_REVIEW = WriteCodeReview
WRITE_TEST = WriteTest

View file

@ -4,49 +4,60 @@
@Time : 2023/5/11 14:43
@Author : alexanderwu
@File : action.py
@Modified By: mashenquan, 2023/8/20. Add function return annotations.
@Modified By: mashenquan, 2023/9/8. Replace LLM with LLMFactory
"""
from __future__ import annotations
from abc import ABC
from typing import Optional
from typing import Any, Optional, Union
from tenacity import retry, stop_after_attempt, wait_random_exponential
from pydantic import BaseModel, Field
from metagpt.actions.action_output import ActionOutput
from metagpt.llm import LLM
from metagpt.logs import logger
from metagpt.provider.base_gpt_api import BaseGPTAPI
from metagpt.provider.postprecess.llm_output_postprecess import llm_output_postprecess
from metagpt.utils.common import OutputParser, general_after_log
from metagpt.schema import (
CodeSummarizeContext,
CodingContext,
RunCodeContext,
TestingContext,
)
action_subclass_registry = {}
class Action(ABC):
def __init__(self, name: str = "", context=None, llm: BaseGPTAPI = None):
self.name: str = name
if llm is None:
llm = LLM()
self.llm = llm
self.context = context
self.prefix = "" # aask*时会加上prefix作为system_message
self.profile = "" # FIXME: USELESS
self.desc = "" # for skill manager
self.nodes = ...
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)
# Output, useless
# self.content = ""
# self.instruct_content = None
# self.env = None
# builtin variables
builtin_class_name: str = ""
# def set_env(self, env):
# self.env = env
class Config:
arbitrary_types_allowed = True
def set_prefix(self, prefix, profile):
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):
@ -62,33 +73,6 @@ class Action(ABC):
system_msgs.append(self.prefix)
return await self.llm.aask(prompt, system_msgs)
@retry(
wait=wait_random_exponential(min=1, max=60),
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,
format="markdown", # compatible to original format
) -> ActionOutput:
content = await self.llm.aask(prompt, system_msgs)
logger.debug(f"llm raw output:\n{content}")
output_class = ActionOutput.create_model_class(output_class_name, output_data_mapping)
if format == "json":
parsed_data = llm_output_postprecess(output=content, schema=output_class.schema(), req_key="[/CONTENT]")
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 ActionOutput(content, instruct_content)
async def run(self, *args, **kwargs) -> str | ActionOutput | None:
async def run(self, *args, **kwargs):
"""Run action"""
raise NotImplementedError("The run method should be implemented in a subclass.")

View file

@ -4,24 +4,26 @@
@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
import re
from typing import Any, Dict, List, Optional, Type
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.actions import ActionOutput
from metagpt.llm import BaseGPTAPI
from metagpt.logs import logger
from metagpt.utils.common import OutputParser
from metagpt.utils.custom_decoder import CustomDecoder
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."
CONSTRAINT = """
- Language: Please use the same language as the user input.
- Format: output wrapped inside [CONTENT][/CONTENT] as format example, nothing else.
"""
SIMPLE_TEMPLATE = """
## context
@ -32,21 +34,21 @@ SIMPLE_TEMPLATE = """
## format example
{example}
## nodes: "<node>: <type> # <comment>"
## nodes: "<node>: <type> # <instruction>"
{instruction}
## constraint
{constraint}
## action
Based on the 'context' content, fill in the {node_name} using the 'format example' format above."
Follow instructions of nodes, generate output and make sure it follows the format example.
"""
def dict_to_markdown(d, prefix="###", postfix="\n"):
def dict_to_markdown(d, prefix="- ", kv_sep="\n", postfix="\n"):
markdown_str = ""
for key, value in d.items():
markdown_str += f"{prefix} {key}: {value}{postfix}"
markdown_str += f"{prefix}{key}{kv_sep}{value}{postfix}"
return markdown_str
@ -76,7 +78,7 @@ class ActionNode:
key: str,
expected_type: Type,
instruction: str,
example: str,
example: Any,
content: str = "",
children: dict[str, "ActionNode"] = None,
):
@ -111,22 +113,22 @@ class ActionNode:
obj.add_children(nodes)
return obj
def get_children_mapping(self) -> Dict[str, Type]:
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, Type]:
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, 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, Type]):
def create_model_class(cls, class_name: str, mapping: Dict[str, Tuple[Type, Any]]):
"""基于pydantic v1的模型动态生成用来检验结果类型正确性"""
new_class = create_model(class_name, **mapping)
@ -148,29 +150,6 @@ class ActionNode:
new_class.__root_validator_check_missing_fields = classmethod(check_missing_fields)
return new_class
@classmethod
def create_model_class_v2(cls, class_name: str, mapping: Dict[str, Type]):
"""基于pydantic v2的模型动态生成用来检验结果类型正确性待验证"""
new_class = create_model(class_name, **mapping)
@model_validator(mode="before")
def check_missing_fields(data):
required_fields = set(mapping.keys())
missing_fields = required_fields - set(data.keys())
if missing_fields:
raise ValueError(f"Missing fields: {missing_fields}")
return data
@field_validator("*")
def check_name(v: Any, field: str) -> Any:
if field not in mapping.keys():
raise ValueError(f"Unrecognized block: {field}")
return v
new_class.__model_validator_check_missing_fields = classmethod(check_missing_fields)
new_class.__field_validator_check_name = classmethod(check_name)
return new_class
def create_children_class(self):
"""使用object内有的字段直接生成model_class"""
class_name = f"{self.key}_AN"
@ -197,46 +176,46 @@ class ActionNode:
return node_dict
# 遍历子节点并递归调用 to_dict 方法
for child_key, child_node in self.children.items():
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, to) -> str:
if to == "json":
def compile_to(self, i: Dict, schema, kv_sep) -> str:
if schema == "json":
return json.dumps(i, indent=4)
elif to == "markdown":
return dict_to_markdown(i)
elif schema == "markdown":
return dict_to_markdown(i, kv_sep=kv_sep)
else:
return str(i)
def tagging(self, text, to, tag="") -> str:
def tagging(self, text, schema, tag="") -> str:
if not tag:
return text
if to == "json":
if schema == "json":
return f"[{tag}]\n" + text + f"\n[/{tag}]"
else:
else: # markdown
return f"[{tag}]\n" + text + f"\n[/{tag}]"
def _compile_f(self, to, mode, tag, format_func) -> str:
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, to)
return self.tagging(text, to, tag)
text = self.compile_to(nodes, schema, kv_sep)
return self.tagging(text, schema, tag)
def compile_instruction(self, to="raw", mode="children", tag="") -> str:
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(to, mode, tag, format_func)
return self._compile_f(schema, mode, tag, format_func, kv_sep=": ")
def compile_example(self, to="raw", mode="children", tag="") -> str:
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(to, mode, tag, format_func)
return self._compile_f(schema, mode, tag, format_func, kv_sep="\n")
def compile(self, context, to="json", mode="children", template=SIMPLE_TEMPLATE) -> str:
def compile(self, context, schema="json", mode="children", template=SIMPLE_TEMPLATE) -> str:
"""
mode: all/root/children
mode="children": 编译所有子节点为一个统一模板包括instruction与example
@ -245,48 +224,47 @@ class ActionNode:
"""
# FIXME: json instruction会带来格式问题"Project name": "web_2048 # 项目名称使用下划线",
self.instruction = self.compile_instruction(to="markdown", mode=mode)
self.example = self.compile_example(to=to, tag="CONTENT", mode=mode)
node_name = "nodes" if template != SIMPLE_TEMPLATE else f'"{list(self.children.keys())[0]}" node'
# 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,
node_name=node_name,
constraint=constraint,
)
return prompt
@retry(wait=wait_random_exponential(min=1, max=10), stop=stop_after_attempt(6))
@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,
format="markdown", # compatible to original format
) -> ActionOutput:
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(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)
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(parsed_data)
logger.debug(f"parsed_data:\n{parsed_data}")
instruct_content = output_class(**parsed_data)
return ActionOutput(content, instruct_content)
return content, instruct_content
def get(self, key):
return self.instruct_content.dict()[key]
@ -302,23 +280,22 @@ class ActionNode:
def set_context(self, context):
self.set_recursive("context", context)
async def simple_fill(self, to, mode):
prompt = self.compile(context=self.context, to=to, mode=mode)
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"
print(prompt)
output = await self._aask_v1(prompt, class_name, mapping, format=to)
self.content = output.content
self.instruct_content = output.instruct_content
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, to="json", mode="auto", strgy="simple"):
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 to: json/markdown, determine example and output format.
: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
@ -334,12 +311,12 @@ class ActionNode:
self.set_context(context)
if strgy == "simple":
return await self.simple_fill(to, mode)
return await self.simple_fill(schema, mode)
elif strgy == "complex":
# 这里隐式假设了拥有children
tmp = {}
for _, i in self.children.items():
child = await i.simple_fill(to, mode)
child = await i.simple_fill(schema, mode)
tmp.update(child.instruct_content.dict())
cls = self.create_children_class()
self.instruct_content = cls(**tmp)

View file

@ -4,40 +4,15 @@
@Time : 2023/7/11 10:03
@Author : chengmaoyu
@File : action_output
@Modified By: mashenquan, 2023/8/20. Allow 'instruct_content' to be blank.
"""
from typing import Dict, Optional, Type
from pydantic import BaseModel, create_model, root_validator, validator
from pydantic import BaseModel
class ActionOutput:
content: str
instruct_content: Optional[BaseModel] = None
instruct_content: BaseModel
def __init__(self, content: str, instruct_content: BaseModel = None):
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

View file

@ -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

View file

@ -10,11 +10,14 @@
"""
import re
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 RunCodeResult
from metagpt.schema import RunCodeContext, RunCodeResult
from metagpt.utils.common import CodeParser
from metagpt.utils.file_repository import FileRepository
@ -47,8 +50,9 @@ Now you should start rewriting the code:
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, *args, **kwargs) -> str:
output_doc = await FileRepository.get_file(

View file

@ -11,6 +11,9 @@
"""
import json
from pathlib import Path
from typing import Optional
from pydantic import Field
from metagpt.actions import Action, ActionOutput
from metagpt.actions.design_api_an import DESIGN_API_NODE
@ -22,16 +25,13 @@ from metagpt.const import (
SYSTEM_DESIGN_FILE_REPO,
SYSTEM_DESIGN_PDF_FILE_REPO,
)
from metagpt.llm import LLM
from metagpt.logs import logger
from metagpt.schema import Document, Documents
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.get_template import get_template
from metagpt.utils.mermaid import mermaid_to_file
# from typing import List
NEW_REQ_TEMPLATE = """
### Legacy Content
{old_design}
@ -42,15 +42,16 @@ NEW_REQ_TEMPLATE = """
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."
)
async def run(self, with_messages, format=CONFIG.prompt_format):
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
@ -80,13 +81,13 @@ class WriteDesign(Action):
# leaving room for global optimization in subsequent steps.
return ActionOutput(content=changed_files.json(), instruct_content=changed_files)
async def _new_system_design(self, context, format=CONFIG.prompt_format):
node = await DESIGN_API_NODE.fill(context=context, llm=self.llm, to=format)
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 _merge(self, prd_doc, system_design_doc, format=CONFIG.prompt_format):
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, to=format)
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

View file

@ -5,6 +5,8 @@
@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
@ -22,7 +24,7 @@ PROJECT_NAME = ActionNode(
FILE_LIST = ActionNode(
key="File list",
expected_type=list[str],
expected_type=List[str],
instruction="Only need relative paths. ALWAYS write a main.py or app.py here",
example=["main.py", "game.py"],
)

View file

@ -1,30 +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

View file

@ -1,51 +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
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

View file

@ -10,5 +10,7 @@ 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

View 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)

View file

@ -9,31 +9,41 @@
"""
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 DEFAULT_WORKSPACE_ROOT, DOCS_FILE_REPO, REQUIREMENT_FILENAME
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):
def __init__(self, name="", context=None, llm=None):
super().__init__(name, context, llm)
"""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):
if not CONFIG.git_repo:
# Create and initialize the workspace folder, initialize the Git environment.
project_name = CONFIG.project_name or FileRepository.new_filename()
workdir = CONFIG.project_path
if not workdir and CONFIG.workspace_path:
workdir = Path(CONFIG.workspace_path) / project_name
workdir = Path(workdir or DEFAULT_WORKSPACE_ROOT / project_name)
if not CONFIG.inc and workdir.exists():
shutil.rmtree(workdir)
CONFIG.git_repo = GitRepository()
CONFIG.git_repo.open(local_path=workdir, auto_init=True)
"""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)

View file

@ -6,35 +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)

View file

@ -9,7 +9,11 @@
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.
"""
import json
from typing import Optional
from pydantic import Field
from metagpt.actions import ActionOutput
from metagpt.actions.action import Action
@ -21,7 +25,9 @@ from metagpt.const import (
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
@ -35,10 +41,11 @@ NEW_REQ_TEMPLATE = """
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)
async def run(self, with_messages, format=CONFIG.prompt_format):
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
@ -85,16 +92,16 @@ class WriteTasks(Action):
await self._save_pdf(task_doc=task_doc)
return task_doc
async def _run_new_tasks(self, context, format=CONFIG.prompt_format):
node = await PM_NODE.fill(context, self.llm, format)
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 _merge(self, system_design_doc, task_doc, format=CONFIG.prompt_format) -> Document:
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, format)
node = await PM_NODE.fill(context, self.llm, schema)
task_doc.content = node.instruct_content.json(ensure_ascii=False)
return task_doc

View file

@ -5,26 +5,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],
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],
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]],
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=[
@ -35,7 +37,7 @@ LOGIC_ANALYSIS = ActionNode(
TASK_LIST = ActionNode(
key="Task list",
expected_type=list[str],
expected_type=List[str],
instruction="Break down the tasks into a list of filenames, prioritized by dependency order.",
example=["game.py", "main.py"],
)

View file

@ -18,10 +18,13 @@
import subprocess
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 RunCodeResult
from metagpt.schema import RunCodeContext, RunCodeResult
from metagpt.utils.exceptions import handle_exception
PROMPT_TEMPLATE = """
@ -74,8 +77,9 @@ standard errors:
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

View file

@ -4,14 +4,19 @@
@Time : 2023/5/23 17:26
@Author : alexanderwu
@File : search_google.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
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 import SearchEngineType
from metagpt.tools.search_engine import SearchEngine
SEARCH_AND_SUMMARIZE_SYSTEM = """### Requirements
@ -55,7 +60,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.
@ -102,16 +106,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.engine = engine or CONFIG.search_engine
name: str = ""
content: Optional[str] = None
llm: BaseGPTAPI = Field(default_factory=LLM)
config: None = Field(default_factory=Config)
engine: Optional[SearchEngineType] = 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 +149,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]),

View file

@ -12,6 +12,7 @@ import ast
import importlib
import traceback
from copy import deepcopy
from typing import Dict, Optional
from metagpt.actions import Action, ActionOutput
from metagpt.learn.skill_loader import Skill
@ -19,12 +20,10 @@ from metagpt.logs import logger
class ArgumentsParingAction(Action):
def __init__(self, last_talk: str, skill: Skill, context=None, llm=None, **kwargs):
super(ArgumentsParingAction, self).__init__(name="", context=context, llm=llm)
self.skill = skill
self.ask = last_talk
self.rsp = None
self.args = None
skill: Skill
ask: str
rsp: Optional[ActionOutput]
args: Optional[Dict]
@property
def prompt(self):
@ -70,25 +69,23 @@ class ArgumentsParingAction(Action):
class SkillAction(Action):
def __init__(self, skill: Skill, args: dict, context=None, llm=None, **kwargs):
super(SkillAction, self).__init__(name="", context=context, llm=llm)
self._skill = skill
self._args = args
self.rsp = None
skill: Skill
args: Dict
rsp: str = ""
async def run(self, *args, **kwargs) -> str | ActionOutput | None:
"""Run action"""
options = deepcopy(kwargs)
if self._args:
for k in self._args.keys():
if self.args:
for k in self.args.keys():
if k in options:
options.pop(k)
try:
self.rsp = await self.find_and_call_function(self._skill.name, args=self._args, **options)
self.rsp = await self.find_and_call_function(self.skill.name, args=self.args, **options)
except Exception as e:
logger.exception(f"{e}, traceback:{traceback.format_exc()}")
self.rsp = f"Error: {e}"
return ActionOutput(content=self.rsp, instruct_content=self._skill.json())
return ActionOutput(content=self.rsp, instruct_content=self.skill.json())
@staticmethod
async def find_and_call_function(function_name, args, **kwargs):

View file

@ -7,12 +7,15 @@
"""
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 = """
@ -89,8 +92,9 @@ flowchart TB
class SummarizeCode(Action):
def __init__(self, name="SummarizeCode", context=None, llm=None):
super().__init__(name, context, llm)
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):

View file

@ -14,8 +14,10 @@
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.
"""
import json
from pydantic import Field
from tenacity import retry, stop_after_attempt, wait_random_exponential
from metagpt.actions.action import Action
@ -27,7 +29,9 @@ from metagpt.const import (
TASK_FILE_REPO,
TEST_OUTPUTS_FILE_REPO,
)
from metagpt.llm import LLM
from metagpt.logs import logger
from metagpt.provider.base_gpt_api import BaseGPTAPI
from metagpt.schema import CodingContext, Document, RunCodeResult
from metagpt.utils.common import CodeParser
from metagpt.utils.file_repository import FileRepository
@ -84,8 +88,9 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc
class WriteCode(Action):
def __init__(self, name="WriteCode", context=None, llm=None):
super().__init__(name, context, llm)
name: str = "WriteCode"
context: Document = Field(default_factory=Document)
llm: BaseGPTAPI = Field(default_factory=LLM)
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
async def write_code(self, prompt) -> str:
@ -126,7 +131,9 @@ class WriteCode(Action):
logger.info(f"Writing {coding_context.filename}..")
code = await self.write_code(prompt)
if not coding_context.code_doc:
coding_context.code_doc = Document(filename=coding_context.filename, root_path=CONFIG.src_workspace)
# 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

View 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())

View file

@ -8,12 +8,15 @@
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.provider.base_gpt_api import BaseGPTAPI
from metagpt.schema import CodingContext
from metagpt.utils.common import CodeParser
@ -32,7 +35,6 @@ ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenc
```
"""
EXAMPLE_AND_INSTRUCTION = """
{format_example}
@ -119,8 +121,9 @@ REWRITE_CODE_TEMPLATE = """
class WriteCodeReview(Action):
def __init__(self, name="WriteCodeReview", context=None, llm=None):
super().__init__(name, context, llm)
name: str = "WriteCodeReview"
context: CodingContext = Field(default_factory=CodingContext)
llm: BaseGPTAPI = Field(default_factory=LLM)
@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):
@ -158,7 +161,8 @@ class WriteCodeReview(Action):
format_example=format_example,
)
logger.info(
f"Code review and rewrite {self.context.code_doc.filename}: {i+1}/{k} | {len(iterative_code)=}, {len(self.context.code_doc.content)=}"
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

View file

@ -10,10 +10,14 @@
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 __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.action_node import ActionNode
@ -32,17 +36,14 @@ from metagpt.const import (
PRDS_FILE_REPO,
REQUIREMENT_FILENAME,
)
from metagpt.llm import LLM
from metagpt.logs import logger
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.get_template import get_template
from metagpt.utils.mermaid import mermaid_to_file
# from typing import List
CONTEXT_TEMPLATE = """
### Project Name
{project_name}
@ -64,15 +65,16 @@ NEW_REQ_TEMPLATE = """
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, with_messages, format=CONFIG.prompt_format, *args, **kwargs) -> ActionOutput | Message:
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 await self._is_bugfix(requirement_doc.content):
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)
@ -111,7 +113,7 @@ class WritePRD(Action):
# optimization in subsequent steps.
return ActionOutput(content=change_files.json(), instruct_content=change_files)
async def _run_new_requirement(self, requirements, format=CONFIG.prompt_format) -> ActionOutput:
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 = ""
@ -121,7 +123,7 @@ class WritePRD(Action):
# 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, to=format)
node = await WRITE_PRD_NODE.fill(context=context, llm=self.llm, schema=schema)
await self._rename_workspace(node)
return node
@ -130,18 +132,20 @@ class WritePRD(Action):
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, format=CONFIG.prompt_format) -> Document:
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, to=format)
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], *args, **kwargs)
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",
@ -182,7 +186,7 @@ class WritePRD(Action):
return
if not CONFIG.project_name:
if isinstance(prd, ActionOutput) or isinstance(prd, ActionNode):
if isinstance(prd, (ActionOutput, ActionNode)):
ws_name = prd.instruct_content.dict()["Project Name"]
else:
ws_name = CodeParser.parse_str(block="Project Name", text=prd)

View file

@ -5,6 +5,7 @@
@Author : alexanderwu
@File : write_prd_an.py
"""
from typing import List
from metagpt.actions.action_node import ActionNode
from metagpt.logs import logger
@ -26,8 +27,8 @@ PROGRAMMING_LANGUAGE = ActionNode(
ORIGINAL_REQUIREMENTS = ActionNode(
key="Original Requirements",
expected_type=str,
instruction="Place the polished, complete original requirements here.",
example="The game should have a leaderboard and multiple difficulty levels.",
instruction="Place the original user's requirements here.",
example="Create a 2048 game",
)
PROJECT_NAME = ActionNode(
@ -39,26 +40,33 @@ PROJECT_NAME = ActionNode(
PRODUCT_GOALS = ActionNode(
key="Product Goals",
expected_type=list[str],
expected_type=List[str],
instruction="Provide up to three clear, orthogonal product goals.",
example=["Create an engaging user experience", "Ensure high performance", "Provide customizable features"],
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 five scenario-based user stories.",
expected_type=List[str],
instruction="Provide up to 3 to 5 scenario-based user stories.",
example=[
"As a user, I want to be able to choose difficulty levels",
"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 analyses for up to seven competitive products.",
example=["Python Snake Game: Simple interface, lacks advanced features"],
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(
@ -86,14 +94,14 @@ REQUIREMENT_ANALYSIS = ActionNode(
key="Requirement Analysis",
expected_type=str,
instruction="Provide a detailed analysis of the requirements.",
example="The product should be user-friendly.",
example="",
)
REQUIREMENT_POOL = ActionNode(
key="Requirement Pool",
expected_type=list[list[str]],
instruction="List down the requirements with their priority (P0, P1, P2).",
example=[["P0", "..."], ["P1", "..."]],
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(
@ -107,7 +115,7 @@ 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="...",
example="",
)
ISSUE_TYPE = ActionNode(

View file

@ -5,20 +5,28 @@
@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

View 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")

View file

@ -7,10 +7,17 @@
@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
@ -36,8 +43,9 @@ you should correctly import the necessary classes based on these file locations!
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)

View file

@ -9,7 +9,9 @@ Provide configuration, singleton
import datetime
import json
import os
import warnings
from copy import deepcopy
from enum import Enum
from pathlib import Path
from typing import Any
from uuid import uuid4
@ -19,6 +21,7 @@ import yaml
from metagpt.const import DEFAULT_WORKSPACE_ROOT, METAGPT_ROOT, OPTIONS
from metagpt.logs import logger
from metagpt.tools import SearchEngineType, WebBrowserEngineType
from metagpt.utils.common import require_python_version
from metagpt.utils.cost_manager import CostManager
from metagpt.utils.singleton import Singleton
@ -35,6 +38,18 @@ class NotConfiguredException(Exception):
super().__init__(self.message)
class LLMProviderEnum(Enum):
OPENAI = "openai"
ANTHROPIC = "anthropic"
SPARK = "spark"
ZHIPUAI = "zhipuai"
FIREWORKS = "fireworks"
OPEN_LLM = "open_llm"
GEMINI = "gemini"
METAGPT = "metagpt"
AZURE_OPENAI = "azure_openai"
class Config(metaclass=Singleton):
"""
Regular usage method:
@ -49,47 +64,63 @@ class Config(metaclass=Singleton):
default_yaml_file = METAGPT_ROOT / "config/config.yaml"
def __init__(self, yaml_file=default_yaml_file, cost_data=""):
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)
# The agent needs to be billed per user, so billing information cannot be destroyed when the session ends.
self.cost_manager = CostManager(**json.loads(cost_data)) if cost_data else CostManager()
logger.info("Config loading done.")
self._update()
logger.info(f"OpenAI API Model: {self.openai_api_model}")
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),
(self.gemini_api_key, LLMProviderEnum.GEMINI), # reuse logic. but not a key
]:
if self._is_valid_llm_key(k):
# logger.debug(f"Use LLMProvider: {v.value}")
if v == LLMProviderEnum.GEMINI and not require_python_version(req_version=(3, 10)):
warnings.warn("Use Gemini requires Python >= 3.10")
if self.openai_api_key and 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):
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")
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")
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)
and (not self.open_llm_api_base)
and (not self.fireworks_api_key or "YOUR_API_KEY" == self.fireworks_api_key)
):
error_info = (
"Set OPENAI_API_KEY or Anthropic_API_KEY or ZHIPUAI_API_KEY first "
"or FIREWORKS_API_KEY or OPEN_LLM_API_BASE"
)
val = self._get("RAISE_NOT_CONFIG_ERROR")
if val is None or val.lower() == "true":
raise NotConfiguredException(error_info)
else: # for agent
logger.warning(error_info)
self.gemini_api_key = self._get("GEMINI_API_KEY")
_ = self.get_default_llm_provider_enum()
self.openai_api_base = self._get("OPENAI_API_BASE")
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-1106-preview")
self.max_tokens_rsp = self._get("MAX_TOKENS", 2048)
self.deployment_name = self._get("DEPLOYMENT_NAME")
self.deployment_id = self._get("DEPLOYMENT_ID")
self.deployment_name = self._get("DEPLOYMENT_NAME", "gpt-4")
self.spark_appid = self._get("SPARK_APPID")
self.spark_api_secret = self._get("SPARK_API_SECRET")
@ -100,7 +131,7 @@ class Config(metaclass=Singleton):
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.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")
@ -124,11 +155,11 @@ 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", "json")
workspace_uid = (
self._get("WORKSPACE_UID") or f"{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid4().hex[-8:]}"
)
self.repair_llm_output = self._get("REPAIR_LLM_OUTPUT", False)
self.prompt_format = self._get("PROMPT_FORMAT", "json")
self.workspace_path = Path(self._get("WORKSPACE_PATH", DEFAULT_WORKSPACE_ROOT))
val = self._get("WORKSPACE_PATH_WITH_UID")
if val and val.lower() == "true": # for agent
@ -136,6 +167,19 @@ class Config(metaclass=Singleton):
self._ensure_workspace_exists()
self.max_auto_summarize_code = self.max_auto_summarize_code or self._get("MAX_AUTO_SUMMARIZE_CODE", 1)
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}")
@ -158,8 +202,8 @@ class Config(metaclass=Singleton):
@staticmethod
def _get(*args, **kwargs):
m = OPTIONS.get()
return m.get(*args, **kwargs)
i = OPTIONS.get()
return i.get(*args, **kwargs)
def get(self, key, *args, **kwargs):
"""Retrieve values from config/key.yaml, config/config.yaml, and environment variables.
@ -173,8 +217,8 @@ class Config(metaclass=Singleton):
OPTIONS.get()[name] = value
def __getattr__(self, name: str) -> Any:
m = OPTIONS.get()
return m.get(name)
i = OPTIONS.get()
return i.get(name)
def set_context(self, options: dict):
"""Update current config"""
@ -193,8 +237,8 @@ class Config(metaclass=Singleton):
def new_environ(self):
"""Return a new os.environ object"""
env = os.environ.copy()
m = self.options
env.update({k: v for k, v in m.items() if isinstance(v, str)})
i = self.options
env.update({k: v for k, v in i.items() if isinstance(v, str)})
return env

View file

@ -55,11 +55,14 @@ DATA_PATH = METAGPT_ROOT / "data"
RESEARCH_PATH = DATA_PATH / "research"
TUTORIAL_PATH = DATA_PATH / "tutorial_docx"
INVOICE_OCR_TABLE_PATH = DATA_PATH / "invoice_table"
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"
@ -110,7 +113,7 @@ COMMAND_TOKENS = 500
BRAIN_MEMORY = "BRAIN_MEMORY"
SKILL_PATH = "SKILL_PATH"
SERPER_API_KEY = "SERPER_API_KEY"
DEFAULT_TOKEN_SIZE = 500
# format
BASE64_FORMAT = "base64"

View file

@ -12,29 +12,85 @@
functionality is to be consolidated into the `Environment` class.
"""
import asyncio
from pathlib import Path
from typing import Iterable, Set
from pydantic import BaseModel, Field
from metagpt.config import CONFIG
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.roles.role import Role, role_subclass_registry
from metagpt.schema import Message
from metagpt.utils.common import is_subscribed
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
"""
roles: dict[str, Role] = Field(default_factory=dict)
members: dict[Role, Set] = Field(default_factory=dict)
history: str = Field(default="") # For debug
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
@ -111,3 +167,8 @@ class Environment(BaseModel):
def set_subscription(self, obj, tags):
"""Set the labels for message to be consumed by the object"""
self.members[obj] = tags
@staticmethod
def archive(auto_archive=True):
if auto_archive and CONFIG.git_repo:
CONFIG.git_repo.archive()

View file

@ -4,50 +4,16 @@
@Time : 2023/5/11 14:45
@Author : alexanderwu
@File : llm.py
@Modified By: mashenquan, 2023
"""
from metagpt.config import CONFIG
from metagpt.provider import LLMType
from metagpt.config import CONFIG, LLMProviderEnum
from metagpt.provider.base_gpt_api import BaseGPTAPI
from metagpt.provider.fireworks_api import FireWorksGPTAPI
from metagpt.provider.human_provider import HumanProvider
from metagpt.provider.metagpt_llm_api import MetaGPTLLMAPI
from metagpt.provider.open_llm_api import OpenLLMGPTAPI
from metagpt.provider.openai_api import OpenAIGPTAPI
from metagpt.provider.spark_api import SparkAPI
from metagpt.provider.zhipuai_api import ZhiPuAIGPTAPI
from metagpt.provider.llm_provider_registry import LLM_REGISTRY
_ = HumanProvider() # Avoid pre-commit error
# Used in agents
class LLMFactory:
@staticmethod
def new_llm() -> "BaseGPTAPI":
# Determine which type of LLM to use based on the validity of the key.
if CONFIG.spark_api_key:
return SparkAPI()
elif CONFIG.zhipuai_api_key:
return ZhiPuAIGPTAPI()
elif CONFIG.open_llm_api_base:
return OpenLLMGPTAPI()
elif CONFIG.fireworks_api_key:
return FireWorksGPTAPI()
# MetaGPT uses the same parameters as OpenAI.
constructors = {
LLMType.OPENAI.value: OpenAIGPTAPI,
LLMType.METAGPT.value: MetaGPTLLMAPI,
}
constructor = constructors.get(CONFIG.LLM_TYPE)
if constructor:
return constructor()
raise RuntimeError("You should config a LLM configuration first")
# Used in metagpt
def LLM() -> "BaseGPTAPI":
"""initialize different LLM instance according to the key field existence"""
return LLMFactory.new_llm()
def LLM(provider: LLMProviderEnum = CONFIG.get_default_llm_provider_enum()) -> BaseGPTAPI:
"""get the default llm provider"""
return LLM_REGISTRY.get_provider(provider)

View file

@ -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 = {
"User": "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)

View file

@ -5,6 +5,10 @@
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
"""
from typing import Optional
from pydantic import Field
from metagpt.logs import logger
from metagpt.memory import Memory
from metagpt.memory.memory_storage import MemoryStorage
@ -18,11 +22,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)
@ -38,7 +43,7 @@ class LongTermMemory(Memory):
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
@ -51,7 +56,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
@ -65,9 +70,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()

View file

@ -7,19 +7,50 @@
@Modified By: mashenquan, 2023-11-1. According to RFC 116: Updated the type of index key.
"""
from collections import defaultdict
from pathlib import Path
from typing import Iterable, Set
from pydantic import BaseModel, Field
from metagpt.schema import Message
from metagpt.utils.common import any_to_str, any_to_str_set
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[str, 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"""
@ -41,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)

View file

@ -61,7 +61,7 @@ class MemoryStorage(FaissStore):
return index_fpath, storage_fpath
def persist(self):
super(MemoryStorage, self).persist()
super().persist()
logger.debug(f"Agent {self.role_id} persist memory into local")
def add(self, message: Message) -> bool:

View file

@ -4,23 +4,12 @@
@Time : 2023/5/5 22:59
@Author : alexanderwu
@File : __init__.py
@Modified By: mashenquan, 2023-12-15. Add LLMType
"""
from enum import Enum
from metagpt.provider.fireworks_api import FireWorksGPTAPI
from metagpt.provider.google_gemini_api import GeminiGPTAPI
from metagpt.provider.open_llm_api import OpenLLMGPTAPI
from metagpt.provider.openai_api import OpenAIGPTAPI
from metagpt.provider.zhipuai_api import ZhiPuAIGPTAPI
class LLMType(Enum):
OPENAI = "OpenAI"
METAGPT = "MetaGPT"
UNKNOWN = "UNKNOWN"
@classmethod
def get(cls, value):
for member in cls:
if member.value == value:
return member
return cls.UNKNOWN
@classmethod
def __missing__(cls, value):
return cls.UNKNOWN
__all__ = ["FireWorksGPTAPI", "GeminiGPTAPI", "OpenLLMGPTAPI", "OpenAIGPTAPI", "ZhiPuAIGPTAPI"]

View file

@ -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",

View file

@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
"""
@Time : 2023/5/5 23:08
@Author : alexanderwu
@File : openai.py
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation;
Change cost control from global to company level.
@Modified By: mashenquan, 2023/11/21. Fix bug: ReadTimeout.
@Modified By: mashenquan, 2023/12/1. Fix bug: Unclosed connection caused by openai 0.x.
"""
from openai import AsyncAzureOpenAI, AzureOpenAI
from openai._base_client import AsyncHttpxClientWrapper, SyncHttpxClientWrapper
from metagpt.config import CONFIG, Config, LLMProviderEnum
from metagpt.provider.llm_provider_registry import register_provider
from metagpt.provider.openai_api import OpenAIGPTAPI, RateLimiter
@register_provider(LLMProviderEnum.AZURE_OPENAI)
class AzureOpenAIGPTAPI(OpenAIGPTAPI):
"""
Check https://platform.openai.com/examples for examples
"""
def __init__(self):
self.config: Config = CONFIG
self.__init_openai()
self.auto_max_tokens = False
# https://learn.microsoft.com/zh-cn/azure/ai-services/openai/how-to/migration?tabs=python-new%2Cdalle-fix
self._client = AsyncAzureOpenAI(
api_key=CONFIG.openai_api_key,
api_version=CONFIG.openai_api_version,
azure_endpoint=CONFIG.openai_api_base,
)
RateLimiter.__init__(self, rpm=self.rpm)
def _make_client(self):
kwargs, async_kwargs = self._make_client_kwargs()
self.client = AzureOpenAI(**kwargs)
self.async_client = AsyncAzureOpenAI(**async_kwargs)
def _make_client_kwargs(self) -> (dict, dict):
kwargs = dict(
api_key=self.config.openai_api_key,
api_version=self.config.openai_api_version,
azure_endpoint=self.config.openai_base_url,
)
async_kwargs = kwargs.copy()
# to use proxy, openai v1 needs http_client
proxy_params = self._get_proxy_params()
if proxy_params:
kwargs["http_client"] = SyncHttpxClientWrapper(**proxy_params)
async_kwargs["http_client"] = AsyncHttpxClientWrapper(**proxy_params)
return kwargs, async_kwargs
def _cons_kwargs(self, messages: list[dict], timeout=3, **configs) -> dict:
kwargs = {
"messages": messages,
"max_tokens": self.get_max_tokens(messages),
"n": 1,
"stop": None,
"temperature": 0.3,
"model": CONFIG.deployment_id,
}
if configs:
kwargs.update(configs)
try:
default_timeout = int(CONFIG.TIMEOUT) if CONFIG.TIMEOUT else 0
except ValueError:
default_timeout = 0
kwargs["timeout"] = max(default_timeout, timeout)
return kwargs

View file

@ -4,18 +4,18 @@
import openai
from metagpt.config import CONFIG
from metagpt.config import CONFIG, LLMProviderEnum
from metagpt.provider.llm_provider_registry import register_provider
from metagpt.provider.openai_api import OpenAIGPTAPI, RateLimiter
from metagpt.utils.cost_manager import CostManager
@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"):

View file

@ -0,0 +1,141 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Desc : Google Gemini LLM from https://ai.google.dev/tutorials/python_quickstart
import google.generativeai as genai
from google.ai import generativelanguage as glm
from google.generativeai.generative_models import GenerativeModel
from google.generativeai.types import content_types
from google.generativeai.types.generation_types import (
AsyncGenerateContentResponse,
GenerateContentResponse,
GenerationConfig,
)
from tenacity import (
after_log,
retry,
retry_if_exception_type,
stop_after_attempt,
wait_random_exponential,
)
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 log_and_reraise
class GeminiGenerativeModel(GenerativeModel):
"""
Due to `https://github.com/google/generative-ai-python/pull/123`, inherit a new class.
Will use default GenerativeModel if it fixed.
"""
def count_tokens(self, contents: content_types.ContentsType) -> glm.CountTokensResponse:
contents = content_types.to_contents(contents)
return self._client.count_tokens(model=self.model_name, contents=contents)
async def count_tokens_async(self, contents: content_types.ContentsType) -> glm.CountTokensResponse:
contents = content_types.to_contents(contents)
return await self._async_client.count_tokens(model=self.model_name, contents=contents)
@register_provider(LLMProviderEnum.GEMINI)
class GeminiGPTAPI(BaseGPTAPI):
"""
Refs to `https://ai.google.dev/tutorials/python_quickstart`
"""
def __init__(self):
self.use_system_prompt = False # google gemini has no system prompt when use api
self.__init_gemini(CONFIG)
self.model = "gemini-pro" # so far only one model
self.llm = GeminiGenerativeModel(model_name=self.model)
def __init_gemini(self, config: CONFIG):
genai.configure(api_key=config.gemini_api_key)
def _user_msg(self, msg: str) -> dict[str, str]:
# Not to change BaseGPTAPI default functions but update with Gemini's conversation format.
# You should follow the format.
return {"role": "user", "parts": [msg]}
def _assistant_msg(self, msg: str) -> dict[str, str]:
return {"role": "model", "parts": [msg]}
def _const_kwargs(self, messages: list[dict], stream: bool = False) -> dict:
kwargs = {"contents": messages, "generation_config": GenerationConfig(temperature=0.3), "stream": stream}
return kwargs
def _update_costs(self, usage: dict):
"""update each request's token cost"""
if CONFIG.calc_usage:
try:
prompt_tokens = int(usage.get("prompt_tokens", 0))
completion_tokens = int(usage.get("completion_tokens", 0))
CONFIG.cost_manager.update_cost(prompt_tokens, completion_tokens, self.model)
except Exception as e:
logger.error(f"google gemini updats costs failed! exp: {e}")
def get_choice_text(self, resp: GenerateContentResponse) -> str:
return resp.text
def get_usage(self, messages: list[dict], resp_text: str) -> dict:
req_text = messages[-1]["parts"][0] if messages else ""
prompt_resp = self.llm.count_tokens(contents={"role": "user", "parts": [{"text": req_text}]})
completion_resp = self.llm.count_tokens(contents={"role": "model", "parts": [{"text": resp_text}]})
usage = {"prompt_tokens": prompt_resp.total_tokens, "completion_tokens": completion_resp.total_tokens}
return usage
async def aget_usage(self, messages: list[dict], resp_text: str) -> dict:
req_text = messages[-1]["parts"][0] if messages else ""
prompt_resp = await self.llm.count_tokens_async(contents={"role": "user", "parts": [{"text": req_text}]})
completion_resp = await self.llm.count_tokens_async(contents={"role": "model", "parts": [{"text": resp_text}]})
usage = {"prompt_tokens": prompt_resp.total_tokens, "completion_tokens": completion_resp.total_tokens}
return usage
def completion(self, messages: list[dict]) -> "GenerateContentResponse":
resp: GenerateContentResponse = self.llm.generate_content(**self._const_kwargs(messages))
usage = self.get_usage(messages, resp.text)
self._update_costs(usage)
return resp
async def _achat_completion(self, messages: list[dict]) -> "AsyncGenerateContentResponse":
resp: AsyncGenerateContentResponse = await self.llm.generate_content_async(**self._const_kwargs(messages))
usage = await self.aget_usage(messages, resp.text)
self._update_costs(usage)
return resp
async def acompletion(self, messages: list[dict]) -> dict:
return await self._achat_completion(messages)
async def _achat_completion_stream(self, messages: list[dict]) -> str:
resp: AsyncGenerateContentResponse = await self.llm.generate_content_async(
**self._const_kwargs(messages, stream=True)
)
collected_content = []
async for chunk in resp:
content = chunk.text
print(content, end="")
collected_content.append(content)
full_content = "".join(collected_content)
usage = await self.aget_usage(messages, full_content)
self._update_costs(usage)
return full_content
@retry(
stop=stop_after_attempt(3),
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,
)
async def acompletion_text(self, messages: list[dict], stream=False) -> str:
"""response in async with stream or non-stream mode"""
if stream:
return await self._achat_completion_stream(messages)
resp = await self._achat_completion(messages)
return self.get_choice_text(resp)

View 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

View file

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
"""
@Time : 2023/5/5 23:08
@Author : alexanderwu
@File : metagpt_api.py
@Desc : MetaGPT LLM provider.
"""
from metagpt.config import LLMProviderEnum
from metagpt.provider import OpenAIGPTAPI
from metagpt.provider.llm_provider_registry import register_provider
@register_provider(LLMProviderEnum.METAGPT)
class METAGPTAPI(OpenAIGPTAPI):
def __init__(self):
super().__init__()

View file

@ -1,279 +0,0 @@
# -*- coding: utf-8 -*-
"""
@Time : 2023/8/30
@Author : mashenquan
@File : metagpt_llm_api.py
@Desc : MetaGPT LLM related APIs
"""
from metagpt.provider.openai_api import OpenAIGPTAPI
# from metagpt.provider.base_gpt_api import BaseGPTAPI
# from metagpt.provider.openai_api import RateLimiter
class MetaGPTLLMAPI(OpenAIGPTAPI):
"""MetaGPT LLM api"""
def __init__(self):
super(MetaGPTLLMAPI, self).__init__()
# def __init__(self):
# self.__init_openai(CONFIG)
# self.llm = openai
# self.model = CONFIG.openai_api_model
# self.auto_max_tokens = False
# self._cost_manager = CostManager()
# RateLimiter.__init__(self, rpm=self.rpm)
#
# def __init_openai(self, config):
# openai.api_key = config.openai_api_key
# if config.openai_api_base:
# openai.api_base = config.openai_api_base
# if config.openai_api_type:
# openai.api_type = config.openai_api_type
# openai.api_version = config.openai_api_version
# self.rpm = int(config.get("RPM", 10))
#
# async def _achat_completion_stream(self, messages: list[dict]) -> str:
# response = await openai.ChatCompletion.acreate(**self._cons_kwargs(messages), stream=True)
#
# # create variables to collect the stream of chunks
# collected_chunks = []
# collected_messages = []
# # iterate through the stream of events
# async for chunk in response:
# collected_chunks.append(chunk) # save the event response
# choices = chunk["choices"]
# if len(choices) > 0:
# chunk_message = chunk["choices"][0].get("delta", {}) # extract the message
# collected_messages.append(chunk_message) # save the message
# if "content" in chunk_message:
# print(chunk_message["content"], end="")
# print()
#
# full_reply_content = "".join([m.get("content", "") for m in collected_messages])
# usage = self._calc_usage(messages, full_reply_content)
# self._update_costs(usage)
# return full_reply_content
#
# def _cons_kwargs(self, messages: list[dict], **configs) -> dict:
# kwargs = {
# "messages": messages,
# "max_tokens": self.get_max_tokens(messages),
# "n": 1,
# "stop": None,
# "temperature": 0.3,
# "timeout": 3,
# }
# if configs:
# kwargs.update(configs)
#
# if CONFIG.openai_api_type == "azure":
# if CONFIG.deployment_name and CONFIG.deployment_id:
# raise ValueError("You can only use one of the `deployment_id` or `deployment_name` model")
# elif not CONFIG.deployment_name and not CONFIG.deployment_id:
# raise ValueError("You must specify `DEPLOYMENT_NAME` or `DEPLOYMENT_ID` parameter")
# kwargs_mode = (
# {"engine": CONFIG.deployment_name}
# if CONFIG.deployment_name
# else {"deployment_id": CONFIG.deployment_id}
# )
# else:
# kwargs_mode = {"model": self.model}
# kwargs.update(kwargs_mode)
# return kwargs
#
# async def _achat_completion(self, messages: list[dict]) -> dict:
# rsp = await self.llm.ChatCompletion.acreate(**self._cons_kwargs(messages))
# self._update_costs(rsp.get("usage"))
# return rsp
#
# def _chat_completion(self, messages: list[dict]) -> dict:
# rsp = self.llm.ChatCompletion.create(**self._cons_kwargs(messages))
# self._update_costs(rsp)
# return rsp
#
# def completion(self, messages: list[dict]) -> dict:
# # if isinstance(messages[0], Message):
# # messages = self.messages_to_dict(messages)
# return self._chat_completion(messages)
#
# async def acompletion(self, messages: list[dict]) -> dict:
# # if isinstance(messages[0], Message):
# # messages = self.messages_to_dict(messages)
# return await self._achat_completion(messages)
#
# @retry(
# 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,
# )
# async def acompletion_text(self, messages: list[dict], stream=False) -> str:
# """when streaming, print each token in place."""
# if stream:
# return await self._achat_completion_stream(messages)
# rsp = await self._achat_completion(messages)
# return self.get_choice_text(rsp)
#
# def _func_configs(self, messages: list[dict], **kwargs) -> dict:
# """
# Note: Keep kwargs consistent with the parameters in the https://platform.openai.com/docs/api-reference/chat/create
# """
# if "tools" not in kwargs:
# configs = {
# "tools": [{"type": "function", "function": GENERAL_FUNCTION_SCHEMA}],
# "tool_choice": GENERAL_TOOL_CHOICE,
# }
# kwargs.update(configs)
#
# return self._cons_kwargs(messages, **kwargs)
#
# def _chat_completion_function(self, messages: list[dict], **kwargs) -> dict:
# rsp = self.llm.ChatCompletion.create(**self._func_configs(messages, **kwargs))
# self._update_costs(rsp.get("usage"))
# return rsp
#
# async def _achat_completion_function(self, messages: list[dict], **chat_configs) -> dict:
# rsp = await self.llm.ChatCompletion.acreate(**self._func_configs(messages, **chat_configs))
# self._update_costs(rsp.get("usage"))
# return rsp
#
# def _process_message(self, messages: Union[str, Message, list[dict], list[Message], list[str]]) -> list[dict]:
# """convert messages to list[dict]."""
# if isinstance(messages, list):
# messages = [Message(msg) if isinstance(msg, str) else msg for msg in messages]
# return [msg if isinstance(msg, dict) else msg.to_dict() for msg in messages]
#
# if isinstance(messages, Message):
# messages = [messages.to_dict()]
# elif isinstance(messages, str):
# messages = [{"role": "user", "content": messages}]
# else:
# raise ValueError(
# f"Only support messages type are: str, Message, list[dict], but got {type(messages).__name__}!"
# )
# return messages
#
# def ask_code(self, messages: Union[str, Message, list[dict]], **kwargs) -> dict:
# """Use function of tools to ask a code.
#
# Note: Keep kwargs consistent with the parameters in the https://platform.openai.com/docs/api-reference/chat/create
#
# Examples:
#
# >>> llm = OpenAIGPTAPI()
# >>> llm.ask_code("Write a python hello world code.")
# {'language': 'python', 'code': "print('Hello, World!')"}
# >>> msg = [{'role': 'user', 'content': "Write a python hello world code."}]
# >>> llm.ask_code(msg)
# {'language': 'python', 'code': "print('Hello, World!')"}
# """
# messages = self._process_message(messages)
# rsp = self._chat_completion_function(messages, **kwargs)
# return self.get_choice_function_arguments(rsp)
#
# async def aask_code(self, messages: Union[str, Message, list[dict]], **kwargs) -> dict:
# """Use function of tools to ask a code.
#
# Note: Keep kwargs consistent with the parameters in the https://platform.openai.com/docs/api-reference/chat/create
#
# Examples:
#
# >>> llm = OpenAIGPTAPI()
# >>> rsp = await llm.ask_code("Write a python hello world code.")
# >>> rsp
# {'language': 'python', 'code': "print('Hello, World!')"}
# >>> msg = [{'role': 'user', 'content': "Write a python hello world code."}]
# >>> rsp = await llm.aask_code(msg) # -> {'language': 'python', 'code': "print('Hello, World!')"}
# """
# messages = self._process_message(messages)
# rsp = await self._achat_completion_function(messages, **kwargs)
# return self.get_choice_function_arguments(rsp)
#
# def _calc_usage(self, messages: list[dict], rsp: str) -> dict:
# usage = {}
# if CONFIG.calc_usage:
# try:
# prompt_tokens = count_message_tokens(messages, self.model)
# completion_tokens = count_string_tokens(rsp, self.model)
# usage["prompt_tokens"] = prompt_tokens
# usage["completion_tokens"] = completion_tokens
# return usage
# except Exception as e:
# logger.error("usage calculation failed!", e)
# else:
# return usage
#
# async def acompletion_batch(self, batch: list[list[dict]]) -> list[dict]:
# """Return full JSON"""
# split_batches = self.split_batches(batch)
# all_results = []
#
# for small_batch in split_batches:
# logger.info(small_batch)
# await self.wait_if_needed(len(small_batch))
#
# future = [self.acompletion(prompt) for prompt in small_batch]
# results = await asyncio.gather(*future)
# logger.info(results)
# all_results.extend(results)
#
# return all_results
#
# async def acompletion_batch_text(self, batch: list[list[dict]]) -> list[str]:
# """Only return plain text"""
# raw_results = await self.acompletion_batch(batch)
# results = []
# for idx, raw_result in enumerate(raw_results, start=1):
# result = self.get_choice_text(raw_result)
# results.append(result)
# logger.info(f"Result of task {idx}: {result}")
# return results
#
# def _update_costs(self, usage: dict):
# if CONFIG.calc_usage:
# try:
# prompt_tokens = int(usage["prompt_tokens"])
# completion_tokens = int(usage["completion_tokens"])
# self._cost_manager.update_cost(prompt_tokens, completion_tokens, self.model)
# except Exception as e:
# logger.error("updating costs failed!", e)
#
# def get_costs(self) -> Costs:
# return self._cost_manager.get_costs()
#
# def get_max_tokens(self, messages: list[dict]):
# if not self.auto_max_tokens:
# return CONFIG.max_tokens_rsp
# return get_max_completion_tokens(messages, self.model, CONFIG.max_tokens_rsp)
#
# def moderation(self, content: Union[str, list[str]]):
# try:
# if not content:
# logger.error("content cannot be empty!")
# else:
# rsp = self._moderation(content=content)
# return rsp
# except Exception as e:
# logger.error(f"moderating failed:{e}")
#
# def _moderation(self, content: Union[str, list[str]]):
# rsp = self.llm.Moderation.create(input=content)
# return rsp
#
# async def amoderation(self, content: Union[str, list[str]]):
# try:
# if not content:
# logger.error("content cannot be empty!")
# else:
# rsp = await self._amoderation(content=content)
# return rsp
# except Exception as e:
# logger.error(f"moderating failed:{e}")
#
# async def _amoderation(self, content: Union[str, list[str]]):
# rsp = await self.llm.Moderation.acreate(input=content)
# return rsp

View file

@ -2,40 +2,39 @@
# -*- coding: utf-8 -*-
# @Desc : self-host open llm model with openai-compatible interface
from metagpt.config import CONFIG
from metagpt.logs import logger
from metagpt.config import CONFIG, LLMProviderEnum
from metagpt.provider.llm_provider_registry import register_provider
from metagpt.provider.openai_api import OpenAIGPTAPI, RateLimiter
from metagpt.utils.cost_manager import CostManager
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
# 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.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"):

View file

@ -10,12 +10,15 @@
"""
import asyncio
import json
import time
from typing import Union
from typing import List, Union
import openai
from openai import APIConnectionError, AsyncAzureOpenAI, AsyncOpenAI, RateLimitError
from openai import APIConnectionError, AsyncOpenAI, AsyncStream, OpenAI, RateLimitError
from openai._base_client import AsyncHttpxClientWrapper, SyncHttpxClientWrapper
from openai.types import CompletionUsage
from openai.types.chat import ChatCompletion, ChatCompletionChunk
from tenacity import (
after_log,
retry,
@ -24,13 +27,15 @@ from tenacity import (
wait_random_exponential,
)
from metagpt.config import CONFIG
from metagpt.config import CONFIG, Config, LLMProviderEnum
from metagpt.const import DEFAULT_MAX_TOKENS
from metagpt.logs import logger
from metagpt.provider import LLMType
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.cost_manager import Costs
from metagpt.utils.exceptions import handle_exception
from metagpt.utils.token_counter import (
count_message_tokens,
count_string_tokens,
@ -74,25 +79,18 @@ 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
"""
def __init__(self):
self.model = CONFIG.openai_api_model
self.config: Config = CONFIG
self.__init_openai()
self.auto_max_tokens = False
self.rpm = int(CONFIG.get("RPM", 10))
if CONFIG.openai_api_type == "azure":
# https://learn.microsoft.com/zh-cn/azure/ai-services/openai/how-to/migration?tabs=python-new%2Cdalle-fix
self._client = AsyncAzureOpenAI(
api_key=CONFIG.openai_api_key,
api_version=CONFIG.openai_api_version,
azure_endpoint=CONFIG.openai_api_base,
)
else:
# https://github.com/openai/openai-python#async-usage
self._client = AsyncOpenAI(api_key=CONFIG.openai_api_key, base_url=CONFIG.openai_api_base)
# https://github.com/openai/openai-python#async-usage
self._client = AsyncOpenAI(api_key=CONFIG.openai_api_key, base_url=CONFIG.openai_api_base)
RateLimiter.__init__(self, rpm=self.rpm)
async def _achat_completion_stream(self, messages: list[dict], timeout=3) -> str:
@ -103,6 +101,59 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
chunk_message = chunk.choices[0].delta.content or "" # extract the message
yield chunk_message
def __init_openai(self):
self.rpm = int(self.config.get("RPM", 10))
self._make_client()
def _make_client(self):
kwargs, async_kwargs = self._make_client_kwargs()
self.client = OpenAI(**kwargs)
self.async_client = AsyncOpenAI(**async_kwargs)
def _make_client_kwargs(self) -> (dict, dict):
kwargs = dict(api_key=self.config.openai_api_key, base_url=self.config.openai_base_url)
async_kwargs = kwargs.copy()
# to use proxy, openai v1 needs http_client
proxy_params = self._get_proxy_params()
if proxy_params:
kwargs["http_client"] = SyncHttpxClientWrapper(**proxy_params)
async_kwargs["http_client"] = AsyncHttpxClientWrapper(**proxy_params)
return kwargs, async_kwargs
def _get_proxy_params(self) -> dict:
params = {}
if self.config.openai_proxy:
params = {"proxies": self.config.openai_proxy}
if self.config.openai_base_url:
params["base_url"] = self.config.openai_base_url
return params
async def _achat_completion_stream(self, messages: list[dict]) -> str:
response: AsyncStream[ChatCompletionChunk] = await self.async_client.chat.completions.create(
**self._cons_kwargs(messages), stream=True
)
# create variables to collect the stream of chunks
collected_chunks = []
collected_messages = []
# iterate through the stream of events
async for chunk in response:
collected_chunks.append(chunk) # save the event response
if chunk.choices:
chunk_message = chunk.choices[0].delta # extract the message
collected_messages.append(chunk_message) # save the message
if chunk_message.content:
print(chunk_message.content, end="")
print()
full_reply_content = "".join([m.content for m in collected_messages if m.content])
usage = self._calc_usage(messages, full_reply_content)
self._update_costs(usage)
return full_reply_content
def _cons_kwargs(self, messages: list[dict], timeout=3, **configs) -> dict:
kwargs = {
"messages": messages,
@ -110,14 +161,10 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
"n": 1,
"stop": None,
"temperature": 0.3,
"model": self.config.openai_api_model,
}
if configs:
kwargs.update(configs)
if CONFIG.openai_api_type == "azure":
kwargs["model"] = CONFIG.deployment_id
else:
kwargs["model"] = self.model
try:
default_timeout = int(CONFIG.TIMEOUT) if CONFIG.TIMEOUT else 0
except ValueError:
@ -126,19 +173,17 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
return kwargs
async def _achat_completion(self, messages: list[dict], timeout=3) -> dict:
async def _achat_completion(self, messages: list[dict], timeout=3) -> ChatCompletion:
kwargs = self._cons_kwargs(messages, timeout=timeout)
rsp = await self._client.chat.completions.create(**kwargs)
rsp: ChatCompletion = await self._client.chat.completions.create(**kwargs)
self._update_costs(rsp.usage)
return rsp.dict()
return rsp
def completion(self, messages: list[dict], timeout=3) -> dict:
def completion(self, messages: list[dict], timeout=3) -> ChatCompletion:
loop = self.get_event_loop()
return loop.run_until_complete(self.acompletion(messages, timeout=timeout))
async def acompletion(self, messages: list[dict], timeout=3) -> dict:
# if isinstance(messages[0], Message):
# messages = self.messages_to_dict(messages)
async def acompletion(self, messages: list[dict], timeout=3) -> ChatCompletion:
return await self._achat_completion(messages, timeout=timeout)
@retry(
@ -188,20 +233,20 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
return self._cons_kwargs(messages=messages, timeout=timeout, **kwargs)
def _chat_completion_function(self, messages: list[dict], timeout=3, **kwargs) -> dict:
def _chat_completion_function(self, messages: list[dict], timeout=3, **kwargs) -> ChatCompletion:
loop = self.get_event_loop()
return loop.run_until_complete(self._achat_completion_function(messages=messages, timeout=timeout, **kwargs))
async def _achat_completion_function(self, messages: list[dict], timeout=3, **chat_configs) -> dict:
async def _achat_completion_function(self, messages: list[dict], timeout=3, **chat_configs) -> ChatCompletion:
kwargs = self._func_configs(messages=messages, timeout=timeout, **chat_configs)
rsp = await self._client.chat.completions.create(**kwargs)
rsp: ChatCompletion = await self._client.chat.completions.create(**kwargs)
self._update_costs(rsp.usage)
return rsp.dict()
return rsp
def _process_message(self, messages: Union[str, Message, list[dict], list[Message], list[str]]) -> list[dict]:
"""convert messages to list[dict]."""
if isinstance(messages, list):
messages = [Message(msg) if isinstance(msg, str) else msg for msg in messages]
messages = [Message(content=msg) if isinstance(msg, str) else msg for msg in messages]
return [msg if isinstance(msg, dict) else msg.to_dict() for msg in messages]
if isinstance(messages, Message):
@ -269,7 +314,35 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
logger.error(f"{self.model} usage calculation failed!", e)
return CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0)
async def acompletion_batch(self, batch: list[list[dict]], timeout=3) -> list[dict]:
def get_choice_function_arguments(self, rsp: ChatCompletion) -> dict:
"""Required to provide the first function arguments of choice.
:return dict: return the first function arguments of choice, for example,
{'language': 'python', 'code': "print('Hello, World!')"}
"""
try:
return json.loads(rsp.choices[0].message.tool_calls[0].function.arguments)
except json.JSONDecodeError:
return {}
def get_choice_text(self, rsp: ChatCompletion) -> str:
"""Required to provide the first text of choice"""
return rsp.choices[0].message.content if rsp.choices else ""
def _calc_usage(self, messages: list[dict], rsp: str) -> CompletionUsage:
usage = CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0)
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]], timeout=3) -> list[ChatCompletion]:
"""Return full JSON"""
split_batches = self.split_batches(batch)
all_results = []
@ -296,11 +369,9 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
return results
def _update_costs(self, usage: CompletionUsage):
if CONFIG.calc_usage:
if CONFIG.calc_usage and usage:
try:
prompt_tokens = usage.prompt_tokens
completion_tokens = usage.completion_tokens
CONFIG.cost_manager.update_cost(prompt_tokens, completion_tokens, self.model)
CONFIG.cost_manager.update_cost(usage.prompt_tokens, usage.completion_tokens, self.model)
except Exception as e:
logger.error("updating costs failed!", e)
@ -316,19 +387,9 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
loop = self.get_event_loop()
loop.run_until_complete(self.amoderation(content=content))
@handle_exception
async def amoderation(self, content: Union[str, list[str]]):
try:
if not content:
logger.error("content cannot be empty!")
else:
rsp = await self._amoderation(content=content)
return rsp
except Exception as e:
logger.error(f"moderating failed:{e}")
async def _amoderation(self, content: Union[str, list[str]]):
rsp = await self._client.moderations.create(input=content)
return rsp
return await self._client.moderations.create(input=content)
async def close(self):
"""Close connection"""
@ -349,8 +410,73 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
else:
raise e
async def get_summary(self, text: str, max_words=200, keep_language: bool = False, **kwargs) -> str:
from metagpt.memory.brain_memory import BrainMemory
async def summarize(self, text: str, max_words=200, keep_language: bool = False, limit: int = -1, **kwargs) -> str:
max_token_count = DEFAULT_MAX_TOKENS
max_count = 100
text_length = len(text)
if limit > 0 and text_length < limit:
return text
summary = ""
while max_count > 0:
if text_length < max_token_count:
summary = await self._get_summary(text=text, max_words=max_words, keep_language=keep_language)
break
memory = BrainMemory(llm_type=LLMType.OPENAI.value, historical_summary=text, cacheable=False)
return await memory.summarize(llm=self, max_words=max_words, keep_language=keep_language)
padding_size = 20 if max_token_count > 20 else 0
text_windows = self.split_texts(text, window_size=max_token_count - padding_size)
part_max_words = min(int(max_words / len(text_windows)) + 1, 100)
summaries = []
for ws in text_windows:
response = await self._get_summary(text=ws, max_words=part_max_words, keep_language=keep_language)
summaries.append(response)
if len(summaries) == 1:
summary = summaries[0]
break
# Merged and retry
text = "\n".join(summaries)
text_length = len(text)
max_count -= 1 # safeguard
return summary
async def _get_summary(self, text: str, max_words=20, keep_language: bool = False):
"""Generate text summary"""
if len(text) < max_words:
return text
if keep_language:
command = f".Translate the above content into a summary of less than {max_words} words in language of the content strictly."
else:
command = f"Translate the above content into a summary of less than {max_words} words."
msg = text + "\n\n" + command
logger.debug(f"summary ask:{msg}")
response = await self.aask(msg=msg, system_msgs=[])
logger.debug(f"summary rsp: {response}")
return response
@staticmethod
def split_texts(text: str, window_size) -> List[str]:
"""Splitting long text into sliding windows text"""
if window_size <= 0:
window_size = DEFAULT_TOKEN_SIZE
total_len = len(text)
if total_len <= window_size:
return [text]
padding_size = 20 if window_size > 20 else 0
windows = []
idx = 0
data_len = window_size - padding_size
while idx < total_len:
if window_size + idx > total_len: # 不足一个滑窗
windows.append(text[idx:])
break
# 每个窗口少算padding_size自然就可实现滑窗功能, 比如: [1, 2, 3, 4, 5, 6, 7, ....]
# window_size=3, padding_size=1
# [1, 2, 3], [3, 4, 5], [5, 6, 7], ....
# idx=2, | idx=5 | idx=8 | ...
w = text[idx : idx + window_size]
windows.append(w)
idx += data_len
return windows

View file

@ -4,7 +4,6 @@
from typing import Union
from metagpt.logs import logger
from metagpt.utils.repair_llm_raw_output import (
RepairType,
extract_content_from_output,
@ -44,7 +43,7 @@ class BasePostPrecessPlugin(object):
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}")
# 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

View file

@ -19,11 +19,13 @@ 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时并不能并行访问。")

View file

@ -15,12 +15,12 @@ from tenacity import (
wait_random_exponential,
)
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 log_and_reraise
from metagpt.provider.zhipuai.zhipu_model_api import ZhiPuModelAPI
from metagpt.utils.cost_manager import CostManager
class ZhiPuEvent(Enum):
@ -30,6 +30,7 @@ class ZhiPuEvent(Enum):
FINISH = "finish"
@register_provider(LLMProviderEnum.ZHIPUAI)
class ZhiPuAIGPTAPI(BaseGPTAPI):
"""
Refs to `https://open.bigmodel.cn/dev/api#chatglm_turbo`
@ -42,7 +43,6 @@ class ZhiPuAIGPTAPI(BaseGPTAPI):
self.__init_zhipuai(CONFIG)
self.llm = ZhiPuModelAPI
self.model = "chatglm_turbo" # so far only one model, just use it
self._cost_manager = CostManager()
def __init_zhipuai(self, config: CONFIG):
assert config.zhipuai_api_key
@ -60,9 +60,9 @@ class ZhiPuAIGPTAPI(BaseGPTAPI):
try:
prompt_tokens = int(usage.get("prompt_tokens", 0))
completion_tokens = int(usage.get("completion_tokens", 0))
self._cost_manager.update_cost(prompt_tokens, completion_tokens, self.model)
CONFIG.cost_manager.update_cost(prompt_tokens, completion_tokens, self.model)
except Exception as e:
logger.error("zhipuai updats costs failed!", e)
logger.error(f"zhipuai updats costs failed! exp: {e}")
def get_choice_text(self, resp: dict) -> str:
"""get the first text of choice from llm response"""

View file

@ -336,4 +336,4 @@ def error():
if __name__ == "__main__":
error()
main()

View file

@ -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,17 +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 software system",
constraints: str = "make sure the architecture is simple enough and use appropriate open source libraries."
"Use same language as user requirement",
) -> 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])

View file

@ -5,6 +5,8 @@
@Author : alexanderwu
@File : sales.py
"""
from typing import Optional
from metagpt.roles import Sales
# from metagpt.actions import SearchAndSummarize
@ -24,5 +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)

View file

@ -16,6 +16,7 @@
@Modified By: mashenquan, 2023-12-5. Enhance the workflow to navigate to WriteCode or QaEngineer based on the results
of SummarizeCode.
"""
from __future__ import annotations
import json
@ -67,24 +68,25 @@ class Engineer(Role):
use_code_review (bool): Whether to use code review.
"""
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 google-style and be modular and maintainable. "
"Use same language as user requirement",
n_borg: int = 1,
use_code_review: bool = False,
) -> None:
"""Initializes the Engineer role with given attributes."""
super().__init__(name, profile, goal, constraints)
self.use_code_review = use_code_review
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._watch([WriteTasks, SummarizeCode, WriteCode, WriteCodeReview, FixBug])
self.code_todos = []
self.summarize_todos = []
self.n_borg = n_borg
self._next_todo = any_to_name(WriteCode)
@staticmethod
@ -307,4 +309,5 @@ class Engineer(Role):
@property
def todo(self) -> str:
"""AgentStore uses this attribute to display to the user what actions the current role should take."""
return self._next_todo

View file

@ -7,10 +7,11 @@
@Modified By: mashenquan, 2023/11/27. Add `PrepareDocuments` action according to Section 2.2.3.5.1 of RFC 135.
"""
from metagpt.actions import UserRequirement, WritePRD
from metagpt.actions.prepare_documents import PrepareDocuments
from metagpt.config import CONFIG
from metagpt.roles import Role
from metagpt.roles.role import Role
from metagpt.utils.common import any_to_name
@ -25,23 +26,13 @@ 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 = "use same language as user requirement",
) -> 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)
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self._init_actions([PrepareDocuments, WritePRD])
self._watch([UserRequirement, PrepareDocuments])
@ -61,4 +52,5 @@ class ProductManager(Role):
@property
def todo(self) -> str:
"""AgentStore uses this attribute to display to the user what actions the current role should take."""
return self._todo

View file

@ -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,23 +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 = "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",
) -> 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])

View file

@ -14,6 +14,8 @@
@Modified By: mashenquan, 2023-12-5. Enhance the workflow to navigate to WriteCode or QaEngineer based on the results
of SummarizeCode.
"""
from metagpt.actions import DebugError, RunCode, WriteTest
from metagpt.actions.summarize_code import SummarizeCode
from metagpt.config import CONFIG
@ -30,21 +32,23 @@ 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
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
test_round: int = 0
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
async def _write_test(self, message: Message) -> None:
src_file_repo = CONFIG.git_repo.new_file_repository(CONFIG.src_workspace)
@ -111,7 +115,8 @@ class QaEngineer(Role):
)
run_code_context.code = None
run_code_context.test_code = None
recipient = parse_recipient(result.summary) # the recipient might be Engineer or myself
# the recipient might be Engineer or myself
recipient = parse_recipient(result.summary)
mappings = {"Engineer": "Alex", "QaEngineer": "Edward"}
self.publish_message(
Message(
@ -178,4 +183,4 @@ class QaEngineer(Role):
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(QaEngineer, self)._observe(ignore_memory=True)
return await super()._observe(ignore_memory=True)

View file

@ -9,7 +9,7 @@ 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
@ -62,24 +62,45 @@ 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=todo)
ret = Message(
content="", instruct_content=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=todo)
ret = Message(
content="", instruct_content=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=self._rc.todo)
ret = Message(
content="",
instruct_content=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

View file

@ -19,20 +19,33 @@
@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 enum import Enum
from typing import Iterable, Set, Type
from pathlib import Path
from typing import Any, Iterable, Set, Type
from pydantic import BaseModel, Field
from metagpt.actions import Action, ActionOutput, UserRequirement
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
from metagpt.provider.base_gpt_api import BaseGPTAPI
from metagpt.schema import Message, MessageQueue
from metagpt.utils.common import any_to_name, any_to_str
from metagpt.utils.common import (
any_to_name,
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}. """
@ -75,34 +88,21 @@ class RoleReactMode(str, Enum):
return [item.value for item in cls]
class RoleSetting(BaseModel):
"""Role properties"""
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)
msg_buffer: MessageQueue = Field(default_factory=MessageQueue) # Message Buffer with Asynchronous Updates
# # 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)
todo: Action = Field(default=None, exclude=True)
watch: set[str] = Field(default_factory=set)
news: list[Type[Message]] = Field(default=[])
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
@ -127,35 +127,154 @@ 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
)
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()
self._states = []
self._actions = []
self._role_id = str(self._setting)
self._rc = RoleContext(watch={any_to_str(UserRequirement)})
self._subscription = {any_to_str(self), name} if name else {any_to_str(self)}
# 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__
if "actions" in kwargs:
self._init_actions(kwargs["actions"])
self._watch(kwargs.get("watch") or [UserRequirement])
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(), self.profile)
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):
if self.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, "
@ -211,7 +330,7 @@ class Role:
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"):
@ -221,35 +340,10 @@ class Role:
if env:
env.set_subscription(self, self._subscription)
@property
def profile(self):
"""Get the role description (position)"""
return self._setting.profile
@property
def name(self):
"""Get virtual user name"""
return self._setting.name
@property
def subscription(self) -> Set:
"""The labels for messages to be consumed by the Role object."""
return self._subscription
@property
def desc(self):
"""Return role `desc`, read only"""
return self._setting.desc
@property
def goal(self):
"""Return role `goal`, read only"""
return self._setting.goal
@property
def constraints(self):
"""Return role `constraints`, read only"""
return self._setting.constraints
return set(self._subscription)
@property
def action_count(self):
@ -258,16 +352,25 @@ class Role:
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) -> bool:
"""Consider what to do and decide on the next course of action. Return false if nothing can be done."""
if len(self._actions) == 1:
# If there is only one action, then only this one can be performed
self._set_state(0)
return True
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 True
prompt = self._get_prefix()
prompt += STATE_TEMPLATE.format(
history=self._rc.history,
@ -275,10 +378,11 @@ class Role:
n_states=len(self._states) - 1,
previous_state=self._rc.state,
)
# print(prompt)
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")
next_state = -1
@ -292,7 +396,7 @@ class Role:
async def _act(self) -> Message:
logger.info(f"{self._setting}: ready to {self._rc.todo}")
response = await self._rc.todo.run(self._rc.important_memory)
if isinstance(response, ActionOutput) or isinstance(response, ActionNode):
if isinstance(response, (ActionOutput, ActionNode)):
msg = Message(
content=response.content,
instruct_content=response.instruct_content,
@ -308,15 +412,30 @@ class Role:
return msg
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
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
# 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 = [n for n in news if n.cause_by in self._rc.watch and n not in old_messages]
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.
@ -347,7 +466,7 @@ class Role:
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()
@ -361,7 +480,8 @@ class Role:
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
@ -369,7 +489,7 @@ class Role:
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"""
@ -386,16 +506,17 @@ class Role:
"""A wrapper to return the most recent k memories of this role, return all when k=0"""
return self._rc.memory.get(k=k)
@role_raise_decorator
async def run(self, with_message=None):
"""Observe, and think and act based on the results of the observation"""
if with_message:
msg = None
if isinstance(with_message, str):
msg = Message(with_message)
msg = Message(content=with_message)
elif isinstance(with_message, Message):
msg = with_message
elif isinstance(with_message, list):
msg = Message("\n".join(with_message))
msg = Message(content="\n".join(with_message))
if not msg.cause_by:
msg.cause_by = UserRequirement
self.put_message(msg)
@ -430,6 +551,7 @@ class Role:
@property
def todo(self) -> str:
"""AgentStore uses this attribute to display to the user what actions the current role should take."""
if self._actions:
return any_to_name(self._actions[0])
return ""

View file

@ -5,30 +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.asearch)
action = SearchAndSummarize(name="", engine=SearchEngineType.CUSTOM_ENGINE, search_func=store.asearch)
else:
action = SearchAndSummarize()
self._init_actions([action])

View file

@ -7,6 +7,9 @@
@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
@ -27,15 +30,13 @@ class Searcher(Role):
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.
@ -46,12 +47,12 @@ 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."""
action = SearchAndSummarize("", engine=SearchEngineType.CUSTOM_ENGINE, search_func=search_func)
action = SearchAndSummarize(name="", engine=SearchEngineType.CUSTOM_ENGINE, search_func=search_func)
self._init_actions([action])
async def _act_sp(self) -> Message:
@ -59,7 +60,7 @@ class Searcher(Role):
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) or isinstance(response, ActionNode):
if isinstance(response, (ActionOutput, ActionNode)):
msg = Message(
content=response.content,
instruct_content=response.instruct_content,

View file

@ -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"))
return msg

View file

@ -12,16 +12,18 @@
between actions.
3. Add `id` to `Message` according to Section 2.2.3.1.1 of RFC 135.
"""
from __future__ import annotations
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 Dict, List, Optional, Set, Type, TypedDict, TypeVar
from typing import Any, Dict, List, Optional, Set, Type, TypedDict, TypeVar
from pydantic import BaseModel, Field
@ -35,8 +37,13 @@ from metagpt.const import (
TASK_FILE_REPO,
)
from metagpt.logs import logger
from metagpt.utils.common import any_to_str, any_to_str_set
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):
@ -97,41 +104,31 @@ class Message(BaseModel):
id: str # According to Section 2.2.3.1.1 of RFC 135
content: str
instruct_content: BaseModel = Field(default=None)
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,
content,
instruct_content=None,
role="user",
cause_by="",
sent_from="",
send_to=MESSAGE_ROUTE_TO_ALL,
**kwargs,
):
"""
Parameters not listed below will be stored as meta info, including custom parameters.
:param content: Message content.
:param instruct_content: Message content struct.
:param cause_by: Message producer
:param sent_from: Message route info tells who sent this message.
:param send_to: Specifies the target recipient or consumer for message delivery in the environment.
:param role: Message meta info tells who sent this message.
"""
super().__init__(
id=uuid.uuid4().hex,
content=content,
instruct_content=instruct_content,
role=role,
cause_by=any_to_str(cause_by),
sent_from=any_to_str(sent_from),
send_to=any_to_str_set(send_to),
**kwargs,
def __init__(self, content: str = "", **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["content"] = kwargs.get("content", content)
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."""
@ -145,8 +142,26 @@ class Message(BaseModel):
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)])
if self.instruct_content:
return f"{self.role}: {self.instruct_content.dict()}"
return f"{self.role}: {self.content}"
def __repr__(self):
@ -164,6 +179,7 @@ class Message(BaseModel):
@handle_exception(exception_type=JSONDecodeError, default_return=None)
def load(val):
"""Convert the json string to object."""
try:
m = json.loads(val)
id = m.get("id")
@ -205,11 +221,22 @@ class AIMessage(Message):
super().__init__(content=content, role="assistant")
class MessageQueue:
class MessageQueue(BaseModel):
"""Message queue which supports asynchronous updates."""
def __init__(self):
self._queue = Queue()
_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."""
@ -257,16 +284,16 @@ class MessageQueue:
return json.dumps(lst)
@staticmethod
def load(i) -> "MessageQueue":
def load(data) -> "MessageQueue":
"""Convert the json string to the `MessageQueue` object."""
queue = MessageQueue()
try:
lst = json.loads(i)
lst = json.loads(data)
for i in lst:
msg = Message(**i)
queue.push(msg)
except JSONDecodeError as e:
logger.warning(f"JSON load failed: {i}, error:{e}")
logger.warning(f"JSON load failed: {data}, error:{e}")
return queue
@ -275,7 +302,7 @@ class MessageQueue:
T = TypeVar("T", bound="BaseModel")
class BaseContext(BaseModel):
class BaseContext(BaseModel, ABC):
@classmethod
@handle_exception
def loads(cls: Type[T], val: str) -> Optional[T]:

View file

@ -7,7 +7,7 @@ import typer
from metagpt.config import CONFIG
app = typer.Typer()
app = typer.Typer(add_completion=False)
@app.command()
@ -22,13 +22,17 @@ def startup(
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.",
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."
),
reqa_file: str = typer.Option(default="", help="Specify the source file name for rewriting the quality test 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.",
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 (
@ -40,30 +44,31 @@ def startup(
)
from metagpt.team import Team
# Use in the PrepareDocuments action according to Section 2.2.3.5.1 of RFC 135.
CONFIG.project_path = project_path
if project_path:
inc = True
project_name = project_name or Path(project_path).name
CONFIG.project_name = project_name
CONFIG.inc = inc
CONFIG.reqa_file = reqa_file
CONFIG.max_auto_summarize_code = max_auto_summarize_code
CONFIG.update_via_cli(project_path, project_name, inc, reqa_file, max_auto_summarize_code)
company = Team()
company.hire(
[
ProductManager(),
Architect(),
ProjectManager(),
]
)
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 implement or code_review:
company.hire([Engineer(n_borg=5, use_code_review=code_review)])
if run_tests:
company.hire([QaEngineer()])
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)

View file

@ -19,7 +19,7 @@ class SubscriptionRunner(BaseModel):
>>> async def trigger():
... while True:
... yield Message("the latest news about OpenAI")
... yield Message(content="the latest news about OpenAI")
... await asyncio.sleep(3600 * 24)
>>> async def callback(msg: Message):

View file

@ -7,24 +7,31 @@
@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 UserRequirement
from metagpt.config import CONFIG
from metagpt.const import MESSAGE_ROUTE_TO_ALL
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.
"""
env: Environment = Field(default_factory=Environment)
@ -34,6 +41,38 @@ class Team(BaseModel):
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.env.add_roles(roles)
@ -77,6 +116,7 @@ class Team(BaseModel):
def _save(self):
logger.info(self.json(ensure_ascii=False))
@serialize_decorator
async def run(self, n_round=3, auto_archive=True):
"""Run company until target round or no money"""
while n_round > 0:
@ -84,7 +124,7 @@ class Team(BaseModel):
n_round -= 1
logger.debug(f"max {n_round=} left.")
self._check_balance()
await self.env.run()
if auto_archive and CONFIG.git_repo:
CONFIG.git_repo.archive()
self.env.archive(auto_archive)
return self.env.history

View file

@ -1,10 +1,10 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Time : 2023/8/17
@Author : mashenquan
@Time : 2023/6/9 22:22
@Author : Leo Xiao
@File : azure_tts.py
@Desc : azure TTS OAS3 api, which provides text-to-speech functionality
@Modified by: mashenquan, 2023/8/17. Azure TTS OAS3 api, which provides text-to-speech functionality
"""
import asyncio
import base64

View file

@ -46,7 +46,6 @@ class OpenCodeInterpreter(object):
interpreter.auto_run = auto_run
interpreter.model = CONFIG.openai_api_model or "gpt-3.5-turbo"
interpreter.api_key = CONFIG.openai_api_key
# interpreter.api_base = CONFIG.openai_api_base
self.interpreter = interpreter
def chat(self, query: str, reset: bool = True):

View file

@ -5,6 +5,7 @@
@Author : zhanglei
@File : moderation.py
"""
import asyncio
from typing import Union
from metagpt.llm import LLM
@ -14,16 +15,6 @@ class Moderation:
def __init__(self):
self.llm = LLM()
def moderation(self, content: Union[str, list[str]]):
resp = []
if content:
moderation_results = self.llm.moderation(content=content)
results = moderation_results.results
for item in results:
resp.append(item.flagged)
return resp
async def amoderation(self, content: Union[str, list[str]]):
resp = []
if content:
@ -35,6 +26,13 @@ class Moderation:
return resp
if __name__ == "__main__":
async def main():
moderation = Moderation()
print(moderation.moderation(content=["I will kill you", "The weather is really nice today", "I want to hit you"]))
rsp = await moderation.amoderation(
content=["I will kill you", "The weather is really nice today", "I want to hit you"]
)
print(rsp)
if __name__ == "__main__":
asyncio.run(main())

View file

@ -111,6 +111,8 @@ def _gen_get_driver_func(browser_type, *args, executable_path=None):
options.add_argument("--headless")
options.add_argument("--enable-javascript")
if browser_type == "chrome":
options.add_argument("--disable-gpu") # This flag can help avoid renderer issue
options.add_argument("--disable-dev-shm-usage") # Overcome limited resource problems
options.add_argument("--no-sandbox")
for i in args:
options.add_argument(i)

View file

@ -13,15 +13,21 @@ from __future__ import annotations
import ast
import contextlib
import importlib
import inspect
import json
import os
import platform
import re
import sys
import traceback
import typing
from pathlib import Path
from typing import Callable, List, Tuple, Union
from typing import Any, Callable, List, Tuple, Union, get_args, get_origin
import aiofiles
import loguru
from pydantic.json import pydantic_encoder
from tenacity import RetryCallState, _utils
from metagpt.config import CONFIG
@ -43,6 +49,12 @@ def check_cmd_exists(command) -> int:
return result
def require_python_version(req_version: tuple[int]) -> bool:
if not (2 <= len(req_version) <= 3):
raise ValueError("req_version should be (3, 9) or (3, 10, 13)")
return True if sys.version_info > req_version else False
class OutputParser:
@classmethod
def parse_blocks(cls, text: str):
@ -130,8 +142,32 @@ class OutputParser:
parsed_data[block] = content
return parsed_data
@staticmethod
def extract_content(text, tag="CONTENT"):
# Use regular expression to extract content between [CONTENT] and [/CONTENT]
extracted_content = re.search(rf"\[{tag}\](.*?)\[/{tag}\]", text, re.DOTALL)
if extracted_content:
return extracted_content.group(1).strip()
else:
return "No content found between [CONTENT] and [/CONTENT] tags."
@staticmethod
def is_supported_list_type(i):
origin = get_origin(i)
if origin is not List:
return False
args = get_args(i)
if args == (str,) or args == (Tuple[str, str],) or args == (List[str],):
return True
return False
@classmethod
def parse_data_with_mapping(cls, data, mapping):
if "[CONTENT]" in data:
data = cls.extract_content(text=data)
block_dict = cls.parse_blocks(data)
parsed_data = {}
for block, content in block_dict.items():
@ -198,7 +234,7 @@ class OutputParser:
result = ast.literal_eval(structure_text)
# Ensure the result matches the specified data type
if isinstance(result, list) or isinstance(result, dict):
if isinstance(result, (list, dict)):
return result
raise ValueError(f"The extracted structure is not a {data_type}.")
@ -437,6 +473,81 @@ def general_after_log(i: "loguru.Logger", sec_format: str = "%0.3f") -> typing.C
return log_it
def read_json_file(json_file: str, encoding=None) -> list[Any]:
if not Path(json_file).exists():
raise FileNotFoundError(f"json_file: {json_file} not exist, return []")
with open(json_file, "r", encoding=encoding) as fin:
try:
data = json.load(fin)
except Exception:
raise ValueError(f"read json file: {json_file} failed")
return data
def write_json_file(json_file: str, data: list, encoding=None):
folder_path = Path(json_file).parent
if not folder_path.exists():
folder_path.mkdir(parents=True, exist_ok=True)
with open(json_file, "w", encoding=encoding) as fout:
json.dump(data, fout, ensure_ascii=False, indent=4, default=pydantic_encoder)
def import_class(class_name: str, module_name: str) -> type:
module = importlib.import_module(module_name)
a_class = getattr(module, class_name)
return a_class
def import_class_inst(class_name: str, module_name: str, *args, **kwargs) -> object:
a_class = import_class(class_name, module_name)
class_inst = a_class(*args, **kwargs)
return class_inst
def format_trackback_info(limit: int = 2):
return traceback.format_exc(limit=limit)
def serialize_decorator(func):
async def wrapper(self, *args, **kwargs):
try:
result = await func(self, *args, **kwargs)
return result
except KeyboardInterrupt:
logger.error(f"KeyboardInterrupt occurs, start to serialize the project, exp:\n{format_trackback_info()}")
except Exception:
logger.error(f"Exception occurs, start to serialize the project, exp:\n{format_trackback_info()}")
self.serialize() # Team.serialize
return wrapper
def role_raise_decorator(func):
async def wrapper(self, *args, **kwargs):
try:
return await func(self, *args, **kwargs)
except KeyboardInterrupt as kbi:
logger.error(f"KeyboardInterrupt: {kbi} occurs, start to serialize the project")
if self.latest_observed_msg:
self._rc.memory.delete(self.latest_observed_msg)
# raise again to make it captured outside
raise Exception(format_trackback_info(limit=None))
except Exception:
if self.latest_observed_msg:
logger.warning(
"There is a exception in role's execution, in order to resume, "
"we delete the newest role communication message in the role's memory."
)
# remove role newest observed msg to make it observed again
self._rc.memory.delete(self.latest_observed_msg)
# raise again to make it captured outside
raise Exception(format_trackback_info(limit=None))
return wrapper
@handle_exception
async def aread(file_path: str) -> str:
"""Read file asynchronously."""

View file

@ -21,6 +21,7 @@ def handle_exception(
_func: Callable[..., ReturnType] = None,
*,
exception_type: Union[Type[Exception], Tuple[Type[Exception], ...]] = Exception,
exception_msg: str = "",
default_return: Any = None,
) -> Callable[..., ReturnType]:
"""handle exception, return default value"""
@ -32,8 +33,9 @@ def handle_exception(
return await func(*args, **kwargs)
except exception_type as e:
logger.opt(depth=1).error(
f"Calling {func.__name__} with args: {args}, kwargs: {kwargs} failed: {e}, "
f"stack: {traceback.format_exc()}"
f"{e}: {exception_msg}, "
f"\nCalling {func.__name__} with args: {args}, kwargs: {kwargs} "
f"\nStack: {traceback.format_exc()}"
)
return default_return

View file

@ -8,10 +8,10 @@
from metagpt.config import CONFIG
def get_template(templates, format=CONFIG.prompt_format):
selected_templates = templates.get(format)
def get_template(templates, schema=CONFIG.prompt_schema):
selected_templates = templates.get(schema)
if selected_templates is None:
raise ValueError(f"Can't find {format} in passed in templates")
raise ValueError(f"Can't find {schema} in passed in templates")
# Extract the selected templates
prompt_template = selected_templates["PROMPT_TEMPLATE"]

View file

@ -21,14 +21,12 @@ def make_sk_kernel():
if CONFIG.openai_api_type == "azure":
kernel.add_chat_service(
"chat_completion",
AzureChatCompletion(CONFIG.deployment_name, CONFIG.openai_api_base, CONFIG.openai_api_key),
AzureChatCompletion(CONFIG.deployment_name, CONFIG.openai_base_url, CONFIG.openai_api_key),
)
else:
kernel.add_chat_service(
"chat_completion",
OpenAIChatCompletion(
CONFIG.openai_api_model, CONFIG.openai_api_key, org_id=None, endpoint=CONFIG.openai_api_base
),
OpenAIChatCompletion(CONFIG.openai_api_model, CONFIG.openai_api_key),
)
return kernel

View file

@ -253,7 +253,7 @@ def retry_parse_json_text(output: str) -> Union[list, dict]:
if CONFIG.repair_llm_output is True, the _aask_v1 and the retry_parse_json_text will loop for {x=3*3} times.
it's a two-layer retry cycle
"""
logger.debug(f"output to json decode:\n{output}")
# logger.debug(f"output to json decode:\n{output}")
# if CONFIG.repair_llm_output is True, it will try to fix output until the retry break
parsed_data = CustomDecoder(strict=False).decode(output)

View file

@ -4,13 +4,11 @@
import copy
import pickle
from typing import Dict, List
from metagpt.actions.action_output import ActionOutput
from metagpt.schema import Message
from metagpt.utils.common import import_class
def actionoutout_schema_to_mapping(schema: Dict) -> Dict:
def actionoutout_schema_to_mapping(schema: dict) -> dict:
"""
directly traverse the `properties` in the first level.
schema structure likes
@ -35,14 +33,31 @@ def actionoutout_schema_to_mapping(schema: Dict) -> Dict:
if property["type"] == "string":
mapping[field] = (str, ...)
elif property["type"] == "array" and property["items"]["type"] == "string":
mapping[field] = (List[str], ...)
mapping[field] = (list[str], ...)
elif property["type"] == "array" and property["items"]["type"] == "array":
# here only consider the `List[List[str]]` situation
mapping[field] = (List[List[str]], ...)
# here only consider the `list[list[str]]` situation
mapping[field] = (list[list[str]], ...)
return mapping
def serialize_message(message: Message):
def actionoutput_mapping_to_str(mapping: dict) -> dict:
new_mapping = {}
for key, value in mapping.items():
new_mapping[key] = str(value)
return new_mapping
def actionoutput_str_to_mapping(mapping: dict) -> dict:
new_mapping = {}
for key, value in mapping.items():
if value == "(<class 'str'>, Ellipsis)":
new_mapping[key] = (str, ...)
else:
new_mapping[key] = eval(value) # `"'(list[str], Ellipsis)"` to `(list[str], ...)`
return new_mapping
def serialize_message(message: "Message"):
message_cp = copy.deepcopy(message) # avoid `instruct_content` value update by reference
ic = message_cp.instruct_content
if ic:
@ -56,11 +71,12 @@ def serialize_message(message: Message):
return msg_ser
def deserialize_message(message_ser: str) -> Message:
def deserialize_message(message_ser: str) -> "Message":
message = pickle.loads(message_ser)
if message.instruct_content:
ic = message.instruct_content
ic_obj = ActionOutput.create_model_class(class_name=ic["class"], 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=ic["mapping"])
ic_new = ic_obj(**ic["value"])
message.instruct_content = ic_new

View file

@ -7,6 +7,7 @@
ref1: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
ref2: https://github.com/Significant-Gravitas/Auto-GPT/blob/master/autogpt/llm/token_counter.py
ref3: https://github.com/hwchase17/langchain/blob/master/langchain/chat_models/openai.py
ref4: https://ai.google.dev/models/gemini
"""
import tiktoken
@ -16,6 +17,8 @@ TOKEN_COSTS = {
"gpt-3.5-turbo-0613": {"prompt": 0.0015, "completion": 0.002},
"gpt-3.5-turbo-16k": {"prompt": 0.003, "completion": 0.004},
"gpt-3.5-turbo-16k-0613": {"prompt": 0.003, "completion": 0.004},
"gpt-35-turbo": {"prompt": 0.0015, "completion": 0.002},
"gpt-35-turbo-16k": {"prompt": 0.003, "completion": 0.004},
"gpt-3.5-turbo-1106": {"prompt": 0.001, "completion": 0.002},
"gpt-4-0314": {"prompt": 0.03, "completion": 0.06},
"gpt-4": {"prompt": 0.03, "completion": 0.06},
@ -25,6 +28,7 @@ TOKEN_COSTS = {
"gpt-4-1106-preview": {"prompt": 0.01, "completion": 0.03},
"text-embedding-ada-002": {"prompt": 0.0004, "completion": 0.0},
"chatglm_turbo": {"prompt": 0.0, "completion": 0.00069}, # 32k version, prompt + completion tokens=0.005¥/k-tokens
"gemini-pro": {"prompt": 0.00025, "completion": 0.0005},
}
@ -34,6 +38,8 @@ TOKEN_MAX = {
"gpt-3.5-turbo-0613": 4096,
"gpt-3.5-turbo-16k": 16384,
"gpt-3.5-turbo-16k-0613": 16384,
"gpt-35-turbo": 4096,
"gpt-35-turbo-16k": 16384,
"gpt-3.5-turbo-1106": 16384,
"gpt-4-0314": 8192,
"gpt-4": 8192,
@ -43,6 +49,7 @@ TOKEN_MAX = {
"gpt-4-1106-preview": 128000,
"text-embedding-ada-002": 8192,
"chatglm_turbo": 32768,
"gemini-pro": 32768,
}
@ -56,6 +63,8 @@ def count_message_tokens(messages, model="gpt-3.5-turbo-0613"):
if model in {
"gpt-3.5-turbo-0613",
"gpt-3.5-turbo-16k-0613",
"gpt-35-turbo",
"gpt-35-turbo-16k",
"gpt-3.5-turbo-16k",
"gpt-3.5-turbo-1106",
"gpt-4-0314",