From 91c5c7208936bacfbeac5938b0a01e1f7ee47c12 Mon Sep 17 00:00:00 2001 From: Arnaud Gelas Date: Mon, 22 Jan 2024 20:10:11 +0100 Subject: [PATCH 01/27] Fix prompt logic when defining to who the message should be sent. With the previous logic, it was possible to reach an undefined state where it was not meant to be sent to Engineer, QaEngineer, nor NoOne. --- metagpt/actions/run_code.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/actions/run_code.py b/metagpt/actions/run_code.py index 30b06f1a6..885f4e12c 100644 --- a/metagpt/actions/run_code.py +++ b/metagpt/actions/run_code.py @@ -42,8 +42,8 @@ Determine the ONE file to rewrite in order to fix the error, for example, xyz.py Determine if all of the code works fine, if so write PASS, else FAIL, WRITE ONLY ONE WORD, PASS OR FAIL, IN THIS SECTION ## Send To: -Please write Engineer if the errors are due to problematic development codes, and QaEngineer to problematic test codes, and NoOne if there are no errors, -WRITE ONLY ONE WORD, Engineer OR QaEngineer OR NoOne, IN THIS SECTION. +Please write NoOne if there are no errors, Engineer if the errors are due to problematic development codes, else QaEngineer, +WRITE ONLY ONE WORD, NoOne OR Engineer OR QaEngineer, IN THIS SECTION. --- You should fill in necessary instruction, status, send to, and finally return all content between the --- segment line. """ From ed54f6b86a7000de1487472a8900138933ac6c42 Mon Sep 17 00:00:00 2001 From: huzixia <528543747@qq.com> Date: Fri, 26 Jan 2024 22:59:10 +0800 Subject: [PATCH 02/27] To avoid JSONDecodeError: Remove comments in output json str, after json value content, maybe start with #, maybe start with //, particularly, it is not inside the string value Addtionly, if you do not want JSONDecodeError to occur, you can add 'Delete comments in json' after FORMAT_CONSTRAINT in action_node.py --- metagpt/actions/action_node.py | 2 ++ metagpt/utils/repair_llm_raw_output.py | 31 ++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/metagpt/actions/action_node.py b/metagpt/actions/action_node.py index 6c65b33ef..0f441cfee 100644 --- a/metagpt/actions/action_node.py +++ b/metagpt/actions/action_node.py @@ -24,6 +24,8 @@ TAG = "CONTENT" LANGUAGE_CONSTRAINT = "Language: Please use the same language as Human INPUT." FORMAT_CONSTRAINT = f"Format: output wrapped inside [{TAG}][/{TAG}] like format example, nothing else." +# Delete comments in json +# If you don't want JSONDecodeError to occur, you can add Delete comments in json after FORMAT_CONSTRAINT SIMPLE_TEMPLATE = """ diff --git a/metagpt/utils/repair_llm_raw_output.py b/metagpt/utils/repair_llm_raw_output.py index b71def136..4995918c2 100644 --- a/metagpt/utils/repair_llm_raw_output.py +++ b/metagpt/utils/repair_llm_raw_output.py @@ -120,13 +120,21 @@ def repair_json_format(output: str) -> str: elif output.startswith("{") and output.endswith("]"): output = output[:-1] + "}" - # remove `#` in output json str, usually appeared in `glm-4` + # remove comments in output json str, after json value content, maybe start with #, maybe start with // arr = output.split("\n") new_arr = [] for line in arr: - idx = line.find("#") - if idx >= 0: - line = line[:idx] + # look for # or // comments and make sure they are not inside the string value + comment_index = -1 + for match in re.finditer(r"(\".*?\"|\'.*?\')|(#|//)", line): + if match.group(1): # if the string value + continue + if match.group(2): # if comments + comment_index = match.start(2) + break + # if comments, then delete them + if comment_index != -1: + line = line[:comment_index].rstrip() new_arr.append(line) output = "\n".join(new_arr) return output @@ -198,6 +206,21 @@ def repair_invalid_json(output: str, error: str) -> str: new_line = line.replace("}", "") elif line.endswith("},") and output.endswith("},"): new_line = line[:-1] + # remove comments in output json str, after json value content, maybe start with #, maybe start with // + elif rline[col_no] == "#" or rline[col_no] == "/": + new_line = rline[:col_no] + for i in range(line_no + 1, len(arr)): + # look for # or // comments and make sure they are not inside the string value + comment_index = -1 + for match in re.finditer(r"(\".*?\"|\'.*?\')|(#|//)", line): + if match.group(1): # if the string value + continue + if match.group(2): # if comments + comment_index = match.start(2) + break + # if comments, then delete them + if comment_index != -1: + arr[i] = arr[i][:comment_index].rstrip() elif (rline[col_no] in ["'", '"']) and (line.startswith('"') or line.startswith("'")) and "," not in line: # problem, `"""` or `'''` without `,` new_line = f",{line}" From 43b069f453d0ea351ba31b918b4fcb8bae5863e0 Mon Sep 17 00:00:00 2001 From: huzixia <528543747@qq.com> Date: Fri, 26 Jan 2024 23:20:16 +0800 Subject: [PATCH 03/27] Addtionly, if you do not want JSONDecodeError to occur, you can add 'Delete comments in json' after FORMAT_CONSTRAINT in action_node.py --- metagpt/actions/action_node.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/metagpt/actions/action_node.py b/metagpt/actions/action_node.py index 0f441cfee..ed0e27869 100644 --- a/metagpt/actions/action_node.py +++ b/metagpt/actions/action_node.py @@ -23,7 +23,8 @@ from metagpt.utils.common import OutputParser, general_after_log TAG = "CONTENT" LANGUAGE_CONSTRAINT = "Language: Please use the same language as Human INPUT." -FORMAT_CONSTRAINT = f"Format: output wrapped inside [{TAG}][/{TAG}] like format example, nothing else." +FORMAT_CONSTRAINT = (f"Format: output wrapped inside [{TAG}][/{TAG}] like format example, nothing else. " + f"Delete comments in json") # Delete comments in json # If you don't want JSONDecodeError to occur, you can add Delete comments in json after FORMAT_CONSTRAINT From f16b24758692bd10d89069c13a1260f9d15c968c Mon Sep 17 00:00:00 2001 From: huzixia <528543747@qq.com> Date: Sat, 27 Jan 2024 15:32:12 +0800 Subject: [PATCH 04/27] merge code with similar logic to avoid duplication --- ...move comments in output json str, after js | 12 + PR/action_node.py | 351 ++++++++++++++++++ PR/repair_llm_raw_output.py | 351 ++++++++++++++++++ metagpt/utils/repair_llm_raw_output.py | 43 ++- 4 files changed, 735 insertions(+), 22 deletions(-) create mode 100644 PR/# remove comments in output json str, after js create mode 100644 PR/action_node.py create mode 100644 PR/repair_llm_raw_output.py diff --git a/PR/# remove comments in output json str, after js b/PR/# remove comments in output json str, after js new file mode 100644 index 000000000..f795fefdb --- /dev/null +++ b/PR/# remove comments in output json str, after js @@ -0,0 +1,12 @@ + +git commit -m "To avoid JSONDecodeError: " -m "Remove comments in output json str, after json value content, maybe start with #, maybe start with //, particularly, it is not inside the string value" -m "Addtionly, if you do not want JSONDecodeError to occur, you can add 'Delete comments in json' after FORMAT_CONSTRAINT in action_node.py" + + + +git commit -m "Addtionly, if you do not want JSONDecodeError to occur, you can add 'Delete comments in json' after FORMAT_CONSTRAINT in action_node.py" + + + + + + diff --git a/PR/action_node.py b/PR/action_node.py new file mode 100644 index 000000000..0f441cfee --- /dev/null +++ b/PR/action_node.py @@ -0,0 +1,351 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2023/12/11 18:45 +@Author : alexanderwu +@File : action_node.py + +NOTE: You should use typing.List instead of list to do type annotation. Because in the markdown extraction process, + we can use typing to extract the type of the node, but we cannot use built-in list to extract. +""" +import json +from typing import Any, Dict, List, Optional, Tuple, Type + +from pydantic import BaseModel, create_model, model_validator +from tenacity import retry, stop_after_attempt, wait_random_exponential + +from metagpt.config import CONFIG +from metagpt.llm import BaseLLM +from metagpt.logs import logger +from metagpt.provider.postprocess.llm_output_postprocess import llm_output_postprocess +from metagpt.utils.common import OutputParser, general_after_log + +TAG = "CONTENT" + +LANGUAGE_CONSTRAINT = "Language: Please use the same language as Human INPUT." +FORMAT_CONSTRAINT = f"Format: output wrapped inside [{TAG}][/{TAG}] like format example, nothing else." +# Delete comments in json +# If you don't want JSONDecodeError to occur, you can add Delete comments in json after FORMAT_CONSTRAINT + + +SIMPLE_TEMPLATE = """ +## context +{context} + +----- + +## format example +{example} + +## nodes: ": # " +{instruction} + +## constraint +{constraint} + +## action +Follow instructions of nodes, generate output and make sure it follows the format example. +""" + + +def dict_to_markdown(d, prefix="- ", kv_sep="\n", postfix="\n"): + markdown_str = "" + for key, value in d.items(): + markdown_str += f"{prefix}{key}{kv_sep}{value}{postfix}" + return markdown_str + + +class ActionNode: + """ActionNode is a tree of nodes.""" + + schema: str # raw/json/markdown, default: "" + + # Action Context + context: str # all the context, including all necessary info + llm: BaseLLM # LLM with aask interface + children: dict[str, "ActionNode"] + + # Action Input + key: str # Product Requirement / File list / Code + expected_type: Type # such as str / int / float etc. + # context: str # everything in the history. + instruction: str # the instructions should be followed. + example: Any # example for In Context-Learning. + + # Action Output + content: str + instruct_content: BaseModel + + def __init__( + self, + key: str, + expected_type: Type, + instruction: str, + example: Any, + content: str = "", + children: dict[str, "ActionNode"] = None, + schema: str = "", + ): + self.key = key + self.expected_type = expected_type + self.instruction = instruction + self.example = example + self.content = content + self.children = children if children is not None else {} + self.schema = schema + + def __str__(self): + return ( + f"{self.key}, {repr(self.expected_type)}, {self.instruction}, {self.example}" + f", {self.content}, {self.children}" + ) + + def __repr__(self): + return self.__str__() + + def add_child(self, node: "ActionNode"): + """增加子ActionNode""" + self.children[node.key] = node + + def add_children(self, nodes: List["ActionNode"]): + """批量增加子ActionNode""" + for node in nodes: + self.add_child(node) + + @classmethod + def from_children(cls, key, nodes: List["ActionNode"]): + """直接从一系列的子nodes初始化""" + obj = cls(key, str, "", "") + obj.add_children(nodes) + return obj + + def get_children_mapping(self, exclude=None) -> Dict[str, Tuple[Type, Any]]: + """获得子ActionNode的字典,以key索引""" + exclude = exclude or [] + return {k: (v.expected_type, ...) for k, v in self.children.items() if k not in exclude} + + 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", exclude=None) -> 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(exclude=exclude) + return {} if exclude and self.key in exclude else self.get_self_mapping() + + @classmethod + def create_model_class(cls, class_name: str, mapping: Dict[str, Tuple[Type, Any]]): + """基于pydantic v1的模型动态生成,用来检验结果类型正确性""" + + def check_fields(cls, values): + required_fields = set(mapping.keys()) + missing_fields = required_fields - set(values.keys()) + if missing_fields: + raise ValueError(f"Missing fields: {missing_fields}") + + unrecognized_fields = set(values.keys()) - required_fields + if unrecognized_fields: + logger.warning(f"Unrecognized fields: {unrecognized_fields}") + return values + + validators = {"check_missing_fields_validator": model_validator(mode="before")(check_fields)} + + new_class = create_model(class_name, __validators__=validators, **mapping) + return new_class + + def create_children_class(self, exclude=None): + """使用object内有的字段直接生成model_class""" + class_name = f"{self.key}_AN" + mapping = self.get_children_mapping(exclude=exclude) + return self.create_model_class(class_name, mapping) + + def to_dict(self, format_func=None, mode="auto", exclude=None) -> Dict: + """将当前节点与子节点都按照node: format的格式组织成字典""" + + # 如果没有提供格式化函数,使用默认的格式化方式 + if format_func is None: + format_func = lambda node: f"{node.instruction}" + + # 使用提供的格式化函数来格式化当前节点的值 + formatted_value = format_func(self) + + # 创建当前节点的键值对 + if mode == "children" or (mode == "auto" and self.children): + node_dict = {} + else: + node_dict = {self.key: formatted_value} + + if mode == "root": + return node_dict + + # 遍历子节点并递归调用 to_dict 方法 + exclude = exclude or [] + for _, child_node in self.children.items(): + if child_node.key in exclude: + continue + node_dict.update(child_node.to_dict(format_func)) + + return node_dict + + def compile_to(self, i: Dict, schema, kv_sep) -> str: + if schema == "json": + return json.dumps(i, indent=4) + elif schema == "markdown": + return dict_to_markdown(i, kv_sep=kv_sep) + else: + return str(i) + + def tagging(self, text, schema, tag="") -> str: + if not tag: + return text + if schema == "json": + return f"[{tag}]\n" + text + f"\n[/{tag}]" + else: # markdown + return f"[{tag}]\n" + text + f"\n[/{tag}]" + + def _compile_f(self, schema, mode, tag, format_func, kv_sep, exclude=None) -> str: + nodes = self.to_dict(format_func=format_func, mode=mode, exclude=exclude) + text = self.compile_to(nodes, schema, kv_sep) + return self.tagging(text, schema, tag) + + def compile_instruction(self, schema="markdown", mode="children", tag="", exclude=None) -> str: + """compile to raw/json/markdown template with all/root/children nodes""" + format_func = lambda i: f"{i.expected_type} # {i.instruction}" + return self._compile_f(schema, mode, tag, format_func, kv_sep=": ", exclude=exclude) + + def compile_example(self, schema="json", mode="children", tag="", exclude=None) -> str: + """compile to raw/json/markdown examples with all/root/children nodes""" + + # 这里不能使用f-string,因为转译为str后再json.dumps会额外加上引号,无法作为有效的example + # 错误示例:"File list": "['main.py', 'const.py', 'game.py']", 注意这里值不是list,而是str + format_func = lambda i: i.example + return self._compile_f(schema, mode, tag, format_func, kv_sep="\n", exclude=exclude) + + def compile(self, context, schema="json", mode="children", template=SIMPLE_TEMPLATE, exclude=[]) -> str: + """ + mode: all/root/children + mode="children": 编译所有子节点为一个统一模板,包括instruction与example + mode="all": NotImplemented + mode="root": NotImplemented + schmea: raw/json/markdown + schema="raw": 不编译,context, lang_constaint, instruction + schema="json":编译context, example(json), instruction(markdown), constraint, action + schema="markdown": 编译context, example(markdown), instruction(markdown), constraint, action + """ + if schema == "raw": + return context + "\n\n## Actions\n" + LANGUAGE_CONSTRAINT + "\n" + self.instruction + + # FIXME: json instruction会带来格式问题,如:"Project name": "web_2048 # 项目名称使用下划线", + # compile example暂时不支持markdown + instruction = self.compile_instruction(schema="markdown", mode=mode, exclude=exclude) + example = self.compile_example(schema=schema, tag=TAG, mode=mode, exclude=exclude) + # nodes = ", ".join(self.to_dict(mode=mode).keys()) + constraints = [LANGUAGE_CONSTRAINT, FORMAT_CONSTRAINT] + constraint = "\n".join(constraints) + + prompt = template.format( + context=context, + example=example, + instruction=instruction, + constraint=constraint, + ) + return prompt + + @retry( + wait=wait_random_exponential(min=1, max=20), + stop=stop_after_attempt(6), + after=general_after_log(logger), + ) + async def _aask_v1( + self, + prompt: str, + output_class_name: str, + output_data_mapping: dict, + system_msgs: Optional[list[str]] = None, + schema="markdown", # compatible to original format + timeout=CONFIG.timeout, + ) -> (str, BaseModel): + """Use ActionOutput to wrap the output of aask""" + content = await self.llm.aask(prompt, system_msgs, timeout=timeout) + 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_postprocess( + output=content, schema=output_class.model_json_schema(), req_key=f"[/{TAG}]" + ) + else: # using markdown parser + parsed_data = OutputParser.parse_data_with_mapping(content, output_data_mapping) + + logger.debug(f"parsed_data:\n{parsed_data}") + instruct_content = output_class(**parsed_data) + return content, instruct_content + + def get(self, key): + return self.instruct_content.model_dump()[key] + + def set_recursive(self, name, value): + setattr(self, name, value) + for _, i in self.children.items(): + i.set_recursive(name, value) + + def set_llm(self, llm): + self.set_recursive("llm", llm) + + def set_context(self, context): + self.set_recursive("context", context) + + async def simple_fill(self, schema, mode, timeout=CONFIG.timeout, exclude=None): + prompt = self.compile(context=self.context, schema=schema, mode=mode, exclude=exclude) + + if schema != "raw": + mapping = self.get_mapping(mode, exclude=exclude) + class_name = f"{self.key}_AN" + content, scontent = await self._aask_v1(prompt, class_name, mapping, schema=schema, timeout=timeout) + self.content = content + self.instruct_content = scontent + else: + self.content = await self.llm.aask(prompt) + self.instruct_content = None + + return self + + async def fill(self, context, llm, schema="json", mode="auto", strgy="simple", timeout=CONFIG.timeout, exclude=[]): + """Fill the node(s) with mode. + + :param context: Everything we should know when filling node. + :param llm: Large Language Model with pre-defined system message. + :param schema: json/markdown, determine example and output format. + - raw: free form text + - json: it's easy to open source LLM with json format + - markdown: when generating code, markdown is always better + :param mode: auto/children/root + - auto: automated fill children's nodes and gather outputs, if no children, fill itself + - children: fill children's nodes and gather outputs + - root: fill root's node and gather output + :param strgy: simple/complex + - simple: run only once + - complex: run each node + :param timeout: Timeout for llm invocation. + :param exclude: The keys of ActionNode to exclude. + :return: self + """ + self.set_llm(llm) + self.set_context(context) + if self.schema: + schema = self.schema + + if strgy == "simple": + return await self.simple_fill(schema=schema, mode=mode, timeout=timeout, exclude=exclude) + elif strgy == "complex": + # 这里隐式假设了拥有children + tmp = {} + for _, i in self.children.items(): + if exclude and i.key in exclude: + continue + child = await i.simple_fill(schema=schema, mode=mode, timeout=timeout, exclude=exclude) + tmp.update(child.instruct_content.dict()) + cls = self.create_children_class() + self.instruct_content = cls(**tmp) + return self diff --git a/PR/repair_llm_raw_output.py b/PR/repair_llm_raw_output.py new file mode 100644 index 000000000..4995918c2 --- /dev/null +++ b/PR/repair_llm_raw_output.py @@ -0,0 +1,351 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Desc : repair llm raw output with particular conditions + +import copy +from enum import Enum +from typing import Callable, Union + +import regex as re +from tenacity import RetryCallState, retry, stop_after_attempt, wait_fixed + +from metagpt.config import CONFIG +from metagpt.logs import logger +from metagpt.utils.custom_decoder import CustomDecoder + + +class RepairType(Enum): + CS = "case sensitivity" + RKPM = "required key pair missing" # condition like `[key] xx` which lacks `[/key]` + SCM = "special character missing" # Usually the req_key appear in pairs like `[key] xx [/key]` + JSON = "json format" + + +def repair_case_sensitivity(output: str, req_key: str) -> str: + """ + usually, req_key is the key name of expected json or markdown content, it won't appear in the value part. + fix target string `"Shared Knowledge": ""` but `"Shared knowledge": ""` actually + """ + if req_key in output: + return output + + output_lower = output.lower() + req_key_lower = req_key.lower() + if req_key_lower in output_lower: + # find the sub-part index, and replace it with raw req_key + lidx = output_lower.find(req_key_lower) + source = output[lidx : lidx + len(req_key_lower)] + output = output.replace(source, req_key) + logger.info(f"repair_case_sensitivity: {req_key}") + + return output + + +def repair_special_character_missing(output: str, req_key: str = "[/CONTENT]") -> str: + """ + fix + 1. target string `[CONTENT] xx [CONTENT] xxx [CONTENT]` lacks `/` in the last `[CONTENT]` + 2. target string `xx [CONTENT] xxx [CONTENT] xxxx` lacks `/` in the last `[CONTENT]` + """ + sc_arr = ["/"] + + if req_key in output: + return output + + for sc in sc_arr: + req_key_pure = req_key.replace(sc, "") + appear_cnt = output.count(req_key_pure) + if req_key_pure in output and appear_cnt > 1: + # req_key with special_character usually in the tail side + ridx = output.rfind(req_key_pure) + output = f"{output[:ridx]}{req_key}{output[ridx + len(req_key_pure):]}" + logger.info(f"repair_special_character_missing: {sc} in {req_key_pure} as position {ridx}") + + return output + + +def repair_required_key_pair_missing(output: str, req_key: str = "[/CONTENT]") -> str: + """ + implement the req_key pair in the begin or end of the content + req_key format + 1. `[req_key]`, and its pair `[/req_key]` + 2. `[/req_key]`, and its pair `[req_key]` + """ + sc = "/" # special char + if req_key.startswith("[") and req_key.endswith("]"): + if sc in req_key: + left_key = req_key.replace(sc, "") # `[/req_key]` -> `[req_key]` + right_key = req_key + else: + left_key = req_key + right_key = f"{req_key[0]}{sc}{req_key[1:]}" # `[req_key]` -> `[/req_key]` + + if left_key not in output: + output = left_key + "\n" + output + if right_key not in output: + + def judge_potential_json(routput: str, left_key: str) -> Union[str, None]: + ridx = routput.rfind(left_key) + if ridx < 0: + return None + sub_output = routput[ridx:] + idx1 = sub_output.rfind("}") + idx2 = sub_output.rindex("]") + idx = idx1 if idx1 >= idx2 else idx2 + sub_output = sub_output[: idx + 1] + return sub_output + + if output.strip().endswith("}") or (output.strip().endswith("]") and not output.strip().endswith(left_key)): + # # avoid [req_key]xx[req_key] case to append [/req_key] + output = output + "\n" + right_key + elif judge_potential_json(output, left_key) and (not output.strip().endswith(left_key)): + sub_content = judge_potential_json(output, left_key) + output = sub_content + "\n" + right_key + + return output + + +def repair_json_format(output: str) -> str: + """ + fix extra `[` or `}` in the end + """ + output = output.strip() + + if output.startswith("[{"): + output = output[1:] + logger.info(f"repair_json_format: {'[{'}") + elif output.endswith("}]"): + output = output[:-1] + logger.info(f"repair_json_format: {'}]'}") + elif output.startswith("{") and output.endswith("]"): + output = output[:-1] + "}" + + # remove comments in output json str, after json value content, maybe start with #, maybe start with // + arr = output.split("\n") + new_arr = [] + for line in arr: + # look for # or // comments and make sure they are not inside the string value + comment_index = -1 + for match in re.finditer(r"(\".*?\"|\'.*?\')|(#|//)", line): + if match.group(1): # if the string value + continue + if match.group(2): # if comments + comment_index = match.start(2) + break + # if comments, then delete them + if comment_index != -1: + line = line[:comment_index].rstrip() + new_arr.append(line) + output = "\n".join(new_arr) + return output + + +def _repair_llm_raw_output(output: str, req_key: str, repair_type: RepairType = None) -> str: + repair_types = [repair_type] if repair_type else [item for item in RepairType if item not in [RepairType.JSON]] + for repair_type in repair_types: + if repair_type == RepairType.CS: + output = repair_case_sensitivity(output, req_key) + elif repair_type == RepairType.RKPM: + output = repair_required_key_pair_missing(output, req_key) + elif repair_type == RepairType.SCM: + output = repair_special_character_missing(output, req_key) + elif repair_type == RepairType.JSON: + output = repair_json_format(output) + return output + + +def repair_llm_raw_output(output: str, req_keys: list[str], repair_type: RepairType = None) -> str: + """ + in open-source llm model, it usually can't follow the instruction well, the output may be incomplete, + so here we try to repair it and use all repair methods by default. + typical case + 1. case sensitivity + target: "Original Requirements" + output: "Original requirements" + 2. special character missing + target: [/CONTENT] + output: [CONTENT] + 3. json format + target: { xxx } + output: { xxx }] + """ + if not CONFIG.repair_llm_output: + return output + + # do the repairation usually for non-openai models + for req_key in req_keys: + output = _repair_llm_raw_output(output=output, req_key=req_key, repair_type=repair_type) + return output + + +def repair_invalid_json(output: str, error: str) -> str: + """ + repair the situation like there are extra chars like + error examples + example 1. json.decoder.JSONDecodeError: Expecting ',' delimiter: line 154 column 1 (char 2765) + example 2. xxx.JSONDecodeError: Expecting property name enclosed in double quotes: line 14 column 1 (char 266) + """ + pattern = r"line ([0-9]+) column ([0-9]+)" + + matches = re.findall(pattern, error, re.DOTALL) + if len(matches) > 0: + line_no = int(matches[0][0]) - 1 + col_no = int(matches[0][1]) - 1 + + # due to CustomDecoder can handle `"": ''` or `'': ""`, so convert `"""` -> `"`, `'''` -> `'` + output = output.replace('"""', '"').replace("'''", '"') + arr = output.split("\n") + rline = arr[line_no] # raw line + line = arr[line_no].strip() + # different general problems + if line.endswith("],"): + # problem, redundant char `]` + new_line = line.replace("]", "") + elif line.endswith("},") and not output.endswith("},"): + # problem, redundant char `}` + new_line = line.replace("}", "") + elif line.endswith("},") and output.endswith("},"): + new_line = line[:-1] + # remove comments in output json str, after json value content, maybe start with #, maybe start with // + elif rline[col_no] == "#" or rline[col_no] == "/": + new_line = rline[:col_no] + for i in range(line_no + 1, len(arr)): + # look for # or // comments and make sure they are not inside the string value + comment_index = -1 + for match in re.finditer(r"(\".*?\"|\'.*?\')|(#|//)", line): + if match.group(1): # if the string value + continue + if match.group(2): # if comments + comment_index = match.start(2) + break + # if comments, then delete them + if comment_index != -1: + arr[i] = arr[i][:comment_index].rstrip() + elif (rline[col_no] in ["'", '"']) and (line.startswith('"') or line.startswith("'")) and "," not in line: + # problem, `"""` or `'''` without `,` + new_line = f",{line}" + elif '",' not in line and "," not in line and '"' not in line: + new_line = f'{line}",' + elif not line.endswith(","): + # problem, miss char `,` at the end. + new_line = f"{line}," + elif "," in line and len(line) == 1: + new_line = f'"{line}' + elif '",' in line: + new_line = line[:-2] + "'," + else: + new_line = line + + arr[line_no] = new_line + output = "\n".join(arr) + logger.info(f"repair_invalid_json, raw error: {error}") + + return output + + +def run_after_exp_and_passon_next_retry(logger: "loguru.Logger") -> Callable[["RetryCallState"], None]: + def run_and_passon(retry_state: RetryCallState) -> None: + """ + RetryCallState example + { + "start_time":143.098322024, + "retry_object":")>", + "fn":"", + "args":"(\"tag:[/CONTENT]\",)", # function input args + "kwargs":{}, # function input kwargs + "attempt_number":1, # retry number + "outcome":"", # type(outcome.result()) = "str", type(outcome.exception()) = "class" + "outcome_timestamp":143.098416904, + "idle_for":0, + "next_action":"None" + } + """ + if retry_state.outcome.failed: + if retry_state.args: + # # can't be used as args=retry_state.args + func_param_output = retry_state.args[0] + elif retry_state.kwargs: + func_param_output = retry_state.kwargs.get("output", "") + exp_str = str(retry_state.outcome.exception()) + + fix_str = "try to fix it, " if CONFIG.repair_llm_output else "" + logger.warning( + f"parse json from content inside [CONTENT][/CONTENT] failed at retry " + f"{retry_state.attempt_number}, {fix_str}exp: {exp_str}" + ) + + repaired_output = repair_invalid_json(func_param_output, exp_str) + retry_state.kwargs["output"] = repaired_output + + return run_and_passon + + +@retry( + stop=stop_after_attempt(3 if CONFIG.repair_llm_output else 0), + wait=wait_fixed(1), + after=run_after_exp_and_passon_next_retry(logger), +) +def retry_parse_json_text(output: str) -> Union[list, dict]: + """ + repair the json-text situation like there are extra chars like [']', '}'] + + Warning + if CONFIG.repair_llm_output is False, retry _aask_v1 {x=3} times, and the retry_parse_json_text's retry not work + 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}") + + # if CONFIG.repair_llm_output is True, it will try to fix output until the retry break + parsed_data = CustomDecoder(strict=False).decode(output) + + return parsed_data + + +def extract_content_from_output(content: str, right_key: str = "[/CONTENT]"): + """extract xxx from [CONTENT](xxx)[/CONTENT] using regex pattern""" + + def re_extract_content(cont: str, pattern: str) -> str: + matches = re.findall(pattern, cont, re.DOTALL) + for match in matches: + if match: + cont = match + break + return cont.strip() + + # TODO construct the extract pattern with the `right_key` + raw_content = copy.deepcopy(content) + pattern = r"\[CONTENT\]([\s\S]*)\[/CONTENT\]" + new_content = re_extract_content(raw_content, pattern) + + if not new_content.startswith("{"): + # TODO find a more general pattern + # # for `[CONTENT]xxx[CONTENT]xxxx[/CONTENT] situation + logger.warning(f"extract_content try another pattern: {pattern}") + if right_key not in new_content: + raw_content = copy.deepcopy(new_content + "\n" + right_key) + # # pattern = r"\[CONTENT\](\s*\{.*?\}\s*)\[/CONTENT\]" + new_content = re_extract_content(raw_content, pattern) + else: + if right_key in new_content: + idx = new_content.find(right_key) + new_content = new_content[:idx] + new_content = new_content.strip() + + return new_content + + +def extract_state_value_from_output(content: str) -> str: + """ + For openai models, they will always return state number. But for open llm models, the instruction result maybe a + long text contain target number, so here add a extraction to improve success rate. + + Args: + content (str): llm's output from `Role._think` + """ + content = content.strip() # deal the output cases like " 0", "0\n" and so on. + pattern = r"([0-9])" # TODO find the number using a more proper method not just extract from content using pattern + matches = re.findall(pattern, content, re.DOTALL) + matches = list(set(matches)) + state = matches[0] if len(matches) > 0 else "-1" + return state diff --git a/metagpt/utils/repair_llm_raw_output.py b/metagpt/utils/repair_llm_raw_output.py index 4995918c2..ef3580750 100644 --- a/metagpt/utils/repair_llm_raw_output.py +++ b/metagpt/utils/repair_llm_raw_output.py @@ -105,6 +105,23 @@ def repair_required_key_pair_missing(output: str, req_key: str = "[/CONTENT]") - return output +def remove_comments_from_line(line): + """ + Remove comments from a single line of string. + Comments are assumed to start with '#' or '//' and are not inside string values. + """ + comment_index = -1 + for match in re.finditer(r"(\".*?\"|\'.*?\')|(#|//)", line): + if match.group(1): # if the string value + continue + if match.group(2): # if comments + comment_index = match.start(2) + break + if comment_index != -1: # if comments, then delete them + return line[:comment_index].rstrip() + return line + + def repair_json_format(output: str) -> str: """ fix extra `[` or `}` in the end @@ -125,17 +142,8 @@ def repair_json_format(output: str) -> str: new_arr = [] for line in arr: # look for # or // comments and make sure they are not inside the string value - comment_index = -1 - for match in re.finditer(r"(\".*?\"|\'.*?\')|(#|//)", line): - if match.group(1): # if the string value - continue - if match.group(2): # if comments - comment_index = match.start(2) - break - # if comments, then delete them - if comment_index != -1: - line = line[:comment_index].rstrip() - new_arr.append(line) + new_line = remove_comments_from_line(line) + new_arr.append(new_line) output = "\n".join(new_arr) return output @@ -209,18 +217,9 @@ def repair_invalid_json(output: str, error: str) -> str: # remove comments in output json str, after json value content, maybe start with #, maybe start with // elif rline[col_no] == "#" or rline[col_no] == "/": new_line = rline[:col_no] + # check the next line and remove the comments for i in range(line_no + 1, len(arr)): - # look for # or // comments and make sure they are not inside the string value - comment_index = -1 - for match in re.finditer(r"(\".*?\"|\'.*?\')|(#|//)", line): - if match.group(1): # if the string value - continue - if match.group(2): # if comments - comment_index = match.start(2) - break - # if comments, then delete them - if comment_index != -1: - arr[i] = arr[i][:comment_index].rstrip() + arr[i] = remove_comments_from_line(arr[i]) elif (rline[col_no] in ["'", '"']) and (line.startswith('"') or line.startswith("'")) and "," not in line: # problem, `"""` or `'''` without `,` new_line = f",{line}" From 8b5f7848fa6aa8c9e0703dcdbf6760ef1efa87eb Mon Sep 17 00:00:00 2001 From: huzixia <528543747@qq.com> Date: Sat, 27 Jan 2024 17:00:59 +0800 Subject: [PATCH 05/27] delete PR dir --- metagpt/utils/repair_llm_raw_output.py | 43 +++++++++++++------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/metagpt/utils/repair_llm_raw_output.py b/metagpt/utils/repair_llm_raw_output.py index 4995918c2..ef3580750 100644 --- a/metagpt/utils/repair_llm_raw_output.py +++ b/metagpt/utils/repair_llm_raw_output.py @@ -105,6 +105,23 @@ def repair_required_key_pair_missing(output: str, req_key: str = "[/CONTENT]") - return output +def remove_comments_from_line(line): + """ + Remove comments from a single line of string. + Comments are assumed to start with '#' or '//' and are not inside string values. + """ + comment_index = -1 + for match in re.finditer(r"(\".*?\"|\'.*?\')|(#|//)", line): + if match.group(1): # if the string value + continue + if match.group(2): # if comments + comment_index = match.start(2) + break + if comment_index != -1: # if comments, then delete them + return line[:comment_index].rstrip() + return line + + def repair_json_format(output: str) -> str: """ fix extra `[` or `}` in the end @@ -125,17 +142,8 @@ def repair_json_format(output: str) -> str: new_arr = [] for line in arr: # look for # or // comments and make sure they are not inside the string value - comment_index = -1 - for match in re.finditer(r"(\".*?\"|\'.*?\')|(#|//)", line): - if match.group(1): # if the string value - continue - if match.group(2): # if comments - comment_index = match.start(2) - break - # if comments, then delete them - if comment_index != -1: - line = line[:comment_index].rstrip() - new_arr.append(line) + new_line = remove_comments_from_line(line) + new_arr.append(new_line) output = "\n".join(new_arr) return output @@ -209,18 +217,9 @@ def repair_invalid_json(output: str, error: str) -> str: # remove comments in output json str, after json value content, maybe start with #, maybe start with // elif rline[col_no] == "#" or rline[col_no] == "/": new_line = rline[:col_no] + # check the next line and remove the comments for i in range(line_no + 1, len(arr)): - # look for # or // comments and make sure they are not inside the string value - comment_index = -1 - for match in re.finditer(r"(\".*?\"|\'.*?\')|(#|//)", line): - if match.group(1): # if the string value - continue - if match.group(2): # if comments - comment_index = match.start(2) - break - # if comments, then delete them - if comment_index != -1: - arr[i] = arr[i][:comment_index].rstrip() + arr[i] = remove_comments_from_line(arr[i]) elif (rline[col_no] in ["'", '"']) and (line.startswith('"') or line.startswith("'")) and "," not in line: # problem, `"""` or `'''` without `,` new_line = f",{line}" From 2361c7e8aa2df55bd3562243e95b4d5f538188ff Mon Sep 17 00:00:00 2001 From: huzixia <528543747@qq.com> Date: Sat, 27 Jan 2024 17:07:21 +0800 Subject: [PATCH 06/27] delete PR dir --- ...move comments in output json str, after js | 12 - PR/action_node.py | 351 ------------------ PR/repair_llm_raw_output.py | 351 ------------------ 3 files changed, 714 deletions(-) delete mode 100644 PR/# remove comments in output json str, after js delete mode 100644 PR/action_node.py delete mode 100644 PR/repair_llm_raw_output.py diff --git a/PR/# remove comments in output json str, after js b/PR/# remove comments in output json str, after js deleted file mode 100644 index f795fefdb..000000000 --- a/PR/# remove comments in output json str, after js +++ /dev/null @@ -1,12 +0,0 @@ - -git commit -m "To avoid JSONDecodeError: " -m "Remove comments in output json str, after json value content, maybe start with #, maybe start with //, particularly, it is not inside the string value" -m "Addtionly, if you do not want JSONDecodeError to occur, you can add 'Delete comments in json' after FORMAT_CONSTRAINT in action_node.py" - - - -git commit -m "Addtionly, if you do not want JSONDecodeError to occur, you can add 'Delete comments in json' after FORMAT_CONSTRAINT in action_node.py" - - - - - - diff --git a/PR/action_node.py b/PR/action_node.py deleted file mode 100644 index 0f441cfee..000000000 --- a/PR/action_node.py +++ /dev/null @@ -1,351 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -@Time : 2023/12/11 18:45 -@Author : alexanderwu -@File : action_node.py - -NOTE: You should use typing.List instead of list to do type annotation. Because in the markdown extraction process, - we can use typing to extract the type of the node, but we cannot use built-in list to extract. -""" -import json -from typing import Any, Dict, List, Optional, Tuple, Type - -from pydantic import BaseModel, create_model, model_validator -from tenacity import retry, stop_after_attempt, wait_random_exponential - -from metagpt.config import CONFIG -from metagpt.llm import BaseLLM -from metagpt.logs import logger -from metagpt.provider.postprocess.llm_output_postprocess import llm_output_postprocess -from metagpt.utils.common import OutputParser, general_after_log - -TAG = "CONTENT" - -LANGUAGE_CONSTRAINT = "Language: Please use the same language as Human INPUT." -FORMAT_CONSTRAINT = f"Format: output wrapped inside [{TAG}][/{TAG}] like format example, nothing else." -# Delete comments in json -# If you don't want JSONDecodeError to occur, you can add Delete comments in json after FORMAT_CONSTRAINT - - -SIMPLE_TEMPLATE = """ -## context -{context} - ------ - -## format example -{example} - -## nodes: ": # " -{instruction} - -## constraint -{constraint} - -## action -Follow instructions of nodes, generate output and make sure it follows the format example. -""" - - -def dict_to_markdown(d, prefix="- ", kv_sep="\n", postfix="\n"): - markdown_str = "" - for key, value in d.items(): - markdown_str += f"{prefix}{key}{kv_sep}{value}{postfix}" - return markdown_str - - -class ActionNode: - """ActionNode is a tree of nodes.""" - - schema: str # raw/json/markdown, default: "" - - # Action Context - context: str # all the context, including all necessary info - llm: BaseLLM # LLM with aask interface - children: dict[str, "ActionNode"] - - # Action Input - key: str # Product Requirement / File list / Code - expected_type: Type # such as str / int / float etc. - # context: str # everything in the history. - instruction: str # the instructions should be followed. - example: Any # example for In Context-Learning. - - # Action Output - content: str - instruct_content: BaseModel - - def __init__( - self, - key: str, - expected_type: Type, - instruction: str, - example: Any, - content: str = "", - children: dict[str, "ActionNode"] = None, - schema: str = "", - ): - self.key = key - self.expected_type = expected_type - self.instruction = instruction - self.example = example - self.content = content - self.children = children if children is not None else {} - self.schema = schema - - def __str__(self): - return ( - f"{self.key}, {repr(self.expected_type)}, {self.instruction}, {self.example}" - f", {self.content}, {self.children}" - ) - - def __repr__(self): - return self.__str__() - - def add_child(self, node: "ActionNode"): - """增加子ActionNode""" - self.children[node.key] = node - - def add_children(self, nodes: List["ActionNode"]): - """批量增加子ActionNode""" - for node in nodes: - self.add_child(node) - - @classmethod - def from_children(cls, key, nodes: List["ActionNode"]): - """直接从一系列的子nodes初始化""" - obj = cls(key, str, "", "") - obj.add_children(nodes) - return obj - - def get_children_mapping(self, exclude=None) -> Dict[str, Tuple[Type, Any]]: - """获得子ActionNode的字典,以key索引""" - exclude = exclude or [] - return {k: (v.expected_type, ...) for k, v in self.children.items() if k not in exclude} - - 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", exclude=None) -> 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(exclude=exclude) - return {} if exclude and self.key in exclude else self.get_self_mapping() - - @classmethod - def create_model_class(cls, class_name: str, mapping: Dict[str, Tuple[Type, Any]]): - """基于pydantic v1的模型动态生成,用来检验结果类型正确性""" - - def check_fields(cls, values): - required_fields = set(mapping.keys()) - missing_fields = required_fields - set(values.keys()) - if missing_fields: - raise ValueError(f"Missing fields: {missing_fields}") - - unrecognized_fields = set(values.keys()) - required_fields - if unrecognized_fields: - logger.warning(f"Unrecognized fields: {unrecognized_fields}") - return values - - validators = {"check_missing_fields_validator": model_validator(mode="before")(check_fields)} - - new_class = create_model(class_name, __validators__=validators, **mapping) - return new_class - - def create_children_class(self, exclude=None): - """使用object内有的字段直接生成model_class""" - class_name = f"{self.key}_AN" - mapping = self.get_children_mapping(exclude=exclude) - return self.create_model_class(class_name, mapping) - - def to_dict(self, format_func=None, mode="auto", exclude=None) -> Dict: - """将当前节点与子节点都按照node: format的格式组织成字典""" - - # 如果没有提供格式化函数,使用默认的格式化方式 - if format_func is None: - format_func = lambda node: f"{node.instruction}" - - # 使用提供的格式化函数来格式化当前节点的值 - formatted_value = format_func(self) - - # 创建当前节点的键值对 - if mode == "children" or (mode == "auto" and self.children): - node_dict = {} - else: - node_dict = {self.key: formatted_value} - - if mode == "root": - return node_dict - - # 遍历子节点并递归调用 to_dict 方法 - exclude = exclude or [] - for _, child_node in self.children.items(): - if child_node.key in exclude: - continue - node_dict.update(child_node.to_dict(format_func)) - - return node_dict - - def compile_to(self, i: Dict, schema, kv_sep) -> str: - if schema == "json": - return json.dumps(i, indent=4) - elif schema == "markdown": - return dict_to_markdown(i, kv_sep=kv_sep) - else: - return str(i) - - def tagging(self, text, schema, tag="") -> str: - if not tag: - return text - if schema == "json": - return f"[{tag}]\n" + text + f"\n[/{tag}]" - else: # markdown - return f"[{tag}]\n" + text + f"\n[/{tag}]" - - def _compile_f(self, schema, mode, tag, format_func, kv_sep, exclude=None) -> str: - nodes = self.to_dict(format_func=format_func, mode=mode, exclude=exclude) - text = self.compile_to(nodes, schema, kv_sep) - return self.tagging(text, schema, tag) - - def compile_instruction(self, schema="markdown", mode="children", tag="", exclude=None) -> str: - """compile to raw/json/markdown template with all/root/children nodes""" - format_func = lambda i: f"{i.expected_type} # {i.instruction}" - return self._compile_f(schema, mode, tag, format_func, kv_sep=": ", exclude=exclude) - - def compile_example(self, schema="json", mode="children", tag="", exclude=None) -> str: - """compile to raw/json/markdown examples with all/root/children nodes""" - - # 这里不能使用f-string,因为转译为str后再json.dumps会额外加上引号,无法作为有效的example - # 错误示例:"File list": "['main.py', 'const.py', 'game.py']", 注意这里值不是list,而是str - format_func = lambda i: i.example - return self._compile_f(schema, mode, tag, format_func, kv_sep="\n", exclude=exclude) - - def compile(self, context, schema="json", mode="children", template=SIMPLE_TEMPLATE, exclude=[]) -> str: - """ - mode: all/root/children - mode="children": 编译所有子节点为一个统一模板,包括instruction与example - mode="all": NotImplemented - mode="root": NotImplemented - schmea: raw/json/markdown - schema="raw": 不编译,context, lang_constaint, instruction - schema="json":编译context, example(json), instruction(markdown), constraint, action - schema="markdown": 编译context, example(markdown), instruction(markdown), constraint, action - """ - if schema == "raw": - return context + "\n\n## Actions\n" + LANGUAGE_CONSTRAINT + "\n" + self.instruction - - # FIXME: json instruction会带来格式问题,如:"Project name": "web_2048 # 项目名称使用下划线", - # compile example暂时不支持markdown - instruction = self.compile_instruction(schema="markdown", mode=mode, exclude=exclude) - example = self.compile_example(schema=schema, tag=TAG, mode=mode, exclude=exclude) - # nodes = ", ".join(self.to_dict(mode=mode).keys()) - constraints = [LANGUAGE_CONSTRAINT, FORMAT_CONSTRAINT] - constraint = "\n".join(constraints) - - prompt = template.format( - context=context, - example=example, - instruction=instruction, - constraint=constraint, - ) - return prompt - - @retry( - wait=wait_random_exponential(min=1, max=20), - stop=stop_after_attempt(6), - after=general_after_log(logger), - ) - async def _aask_v1( - self, - prompt: str, - output_class_name: str, - output_data_mapping: dict, - system_msgs: Optional[list[str]] = None, - schema="markdown", # compatible to original format - timeout=CONFIG.timeout, - ) -> (str, BaseModel): - """Use ActionOutput to wrap the output of aask""" - content = await self.llm.aask(prompt, system_msgs, timeout=timeout) - 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_postprocess( - output=content, schema=output_class.model_json_schema(), req_key=f"[/{TAG}]" - ) - else: # using markdown parser - parsed_data = OutputParser.parse_data_with_mapping(content, output_data_mapping) - - logger.debug(f"parsed_data:\n{parsed_data}") - instruct_content = output_class(**parsed_data) - return content, instruct_content - - def get(self, key): - return self.instruct_content.model_dump()[key] - - def set_recursive(self, name, value): - setattr(self, name, value) - for _, i in self.children.items(): - i.set_recursive(name, value) - - def set_llm(self, llm): - self.set_recursive("llm", llm) - - def set_context(self, context): - self.set_recursive("context", context) - - async def simple_fill(self, schema, mode, timeout=CONFIG.timeout, exclude=None): - prompt = self.compile(context=self.context, schema=schema, mode=mode, exclude=exclude) - - if schema != "raw": - mapping = self.get_mapping(mode, exclude=exclude) - class_name = f"{self.key}_AN" - content, scontent = await self._aask_v1(prompt, class_name, mapping, schema=schema, timeout=timeout) - self.content = content - self.instruct_content = scontent - else: - self.content = await self.llm.aask(prompt) - self.instruct_content = None - - return self - - async def fill(self, context, llm, schema="json", mode="auto", strgy="simple", timeout=CONFIG.timeout, exclude=[]): - """Fill the node(s) with mode. - - :param context: Everything we should know when filling node. - :param llm: Large Language Model with pre-defined system message. - :param schema: json/markdown, determine example and output format. - - raw: free form text - - json: it's easy to open source LLM with json format - - markdown: when generating code, markdown is always better - :param mode: auto/children/root - - auto: automated fill children's nodes and gather outputs, if no children, fill itself - - children: fill children's nodes and gather outputs - - root: fill root's node and gather output - :param strgy: simple/complex - - simple: run only once - - complex: run each node - :param timeout: Timeout for llm invocation. - :param exclude: The keys of ActionNode to exclude. - :return: self - """ - self.set_llm(llm) - self.set_context(context) - if self.schema: - schema = self.schema - - if strgy == "simple": - return await self.simple_fill(schema=schema, mode=mode, timeout=timeout, exclude=exclude) - elif strgy == "complex": - # 这里隐式假设了拥有children - tmp = {} - for _, i in self.children.items(): - if exclude and i.key in exclude: - continue - child = await i.simple_fill(schema=schema, mode=mode, timeout=timeout, exclude=exclude) - tmp.update(child.instruct_content.dict()) - cls = self.create_children_class() - self.instruct_content = cls(**tmp) - return self diff --git a/PR/repair_llm_raw_output.py b/PR/repair_llm_raw_output.py deleted file mode 100644 index 4995918c2..000000000 --- a/PR/repair_llm_raw_output.py +++ /dev/null @@ -1,351 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# @Desc : repair llm raw output with particular conditions - -import copy -from enum import Enum -from typing import Callable, Union - -import regex as re -from tenacity import RetryCallState, retry, stop_after_attempt, wait_fixed - -from metagpt.config import CONFIG -from metagpt.logs import logger -from metagpt.utils.custom_decoder import CustomDecoder - - -class RepairType(Enum): - CS = "case sensitivity" - RKPM = "required key pair missing" # condition like `[key] xx` which lacks `[/key]` - SCM = "special character missing" # Usually the req_key appear in pairs like `[key] xx [/key]` - JSON = "json format" - - -def repair_case_sensitivity(output: str, req_key: str) -> str: - """ - usually, req_key is the key name of expected json or markdown content, it won't appear in the value part. - fix target string `"Shared Knowledge": ""` but `"Shared knowledge": ""` actually - """ - if req_key in output: - return output - - output_lower = output.lower() - req_key_lower = req_key.lower() - if req_key_lower in output_lower: - # find the sub-part index, and replace it with raw req_key - lidx = output_lower.find(req_key_lower) - source = output[lidx : lidx + len(req_key_lower)] - output = output.replace(source, req_key) - logger.info(f"repair_case_sensitivity: {req_key}") - - return output - - -def repair_special_character_missing(output: str, req_key: str = "[/CONTENT]") -> str: - """ - fix - 1. target string `[CONTENT] xx [CONTENT] xxx [CONTENT]` lacks `/` in the last `[CONTENT]` - 2. target string `xx [CONTENT] xxx [CONTENT] xxxx` lacks `/` in the last `[CONTENT]` - """ - sc_arr = ["/"] - - if req_key in output: - return output - - for sc in sc_arr: - req_key_pure = req_key.replace(sc, "") - appear_cnt = output.count(req_key_pure) - if req_key_pure in output and appear_cnt > 1: - # req_key with special_character usually in the tail side - ridx = output.rfind(req_key_pure) - output = f"{output[:ridx]}{req_key}{output[ridx + len(req_key_pure):]}" - logger.info(f"repair_special_character_missing: {sc} in {req_key_pure} as position {ridx}") - - return output - - -def repair_required_key_pair_missing(output: str, req_key: str = "[/CONTENT]") -> str: - """ - implement the req_key pair in the begin or end of the content - req_key format - 1. `[req_key]`, and its pair `[/req_key]` - 2. `[/req_key]`, and its pair `[req_key]` - """ - sc = "/" # special char - if req_key.startswith("[") and req_key.endswith("]"): - if sc in req_key: - left_key = req_key.replace(sc, "") # `[/req_key]` -> `[req_key]` - right_key = req_key - else: - left_key = req_key - right_key = f"{req_key[0]}{sc}{req_key[1:]}" # `[req_key]` -> `[/req_key]` - - if left_key not in output: - output = left_key + "\n" + output - if right_key not in output: - - def judge_potential_json(routput: str, left_key: str) -> Union[str, None]: - ridx = routput.rfind(left_key) - if ridx < 0: - return None - sub_output = routput[ridx:] - idx1 = sub_output.rfind("}") - idx2 = sub_output.rindex("]") - idx = idx1 if idx1 >= idx2 else idx2 - sub_output = sub_output[: idx + 1] - return sub_output - - if output.strip().endswith("}") or (output.strip().endswith("]") and not output.strip().endswith(left_key)): - # # avoid [req_key]xx[req_key] case to append [/req_key] - output = output + "\n" + right_key - elif judge_potential_json(output, left_key) and (not output.strip().endswith(left_key)): - sub_content = judge_potential_json(output, left_key) - output = sub_content + "\n" + right_key - - return output - - -def repair_json_format(output: str) -> str: - """ - fix extra `[` or `}` in the end - """ - output = output.strip() - - if output.startswith("[{"): - output = output[1:] - logger.info(f"repair_json_format: {'[{'}") - elif output.endswith("}]"): - output = output[:-1] - logger.info(f"repair_json_format: {'}]'}") - elif output.startswith("{") and output.endswith("]"): - output = output[:-1] + "}" - - # remove comments in output json str, after json value content, maybe start with #, maybe start with // - arr = output.split("\n") - new_arr = [] - for line in arr: - # look for # or // comments and make sure they are not inside the string value - comment_index = -1 - for match in re.finditer(r"(\".*?\"|\'.*?\')|(#|//)", line): - if match.group(1): # if the string value - continue - if match.group(2): # if comments - comment_index = match.start(2) - break - # if comments, then delete them - if comment_index != -1: - line = line[:comment_index].rstrip() - new_arr.append(line) - output = "\n".join(new_arr) - return output - - -def _repair_llm_raw_output(output: str, req_key: str, repair_type: RepairType = None) -> str: - repair_types = [repair_type] if repair_type else [item for item in RepairType if item not in [RepairType.JSON]] - for repair_type in repair_types: - if repair_type == RepairType.CS: - output = repair_case_sensitivity(output, req_key) - elif repair_type == RepairType.RKPM: - output = repair_required_key_pair_missing(output, req_key) - elif repair_type == RepairType.SCM: - output = repair_special_character_missing(output, req_key) - elif repair_type == RepairType.JSON: - output = repair_json_format(output) - return output - - -def repair_llm_raw_output(output: str, req_keys: list[str], repair_type: RepairType = None) -> str: - """ - in open-source llm model, it usually can't follow the instruction well, the output may be incomplete, - so here we try to repair it and use all repair methods by default. - typical case - 1. case sensitivity - target: "Original Requirements" - output: "Original requirements" - 2. special character missing - target: [/CONTENT] - output: [CONTENT] - 3. json format - target: { xxx } - output: { xxx }] - """ - if not CONFIG.repair_llm_output: - return output - - # do the repairation usually for non-openai models - for req_key in req_keys: - output = _repair_llm_raw_output(output=output, req_key=req_key, repair_type=repair_type) - return output - - -def repair_invalid_json(output: str, error: str) -> str: - """ - repair the situation like there are extra chars like - error examples - example 1. json.decoder.JSONDecodeError: Expecting ',' delimiter: line 154 column 1 (char 2765) - example 2. xxx.JSONDecodeError: Expecting property name enclosed in double quotes: line 14 column 1 (char 266) - """ - pattern = r"line ([0-9]+) column ([0-9]+)" - - matches = re.findall(pattern, error, re.DOTALL) - if len(matches) > 0: - line_no = int(matches[0][0]) - 1 - col_no = int(matches[0][1]) - 1 - - # due to CustomDecoder can handle `"": ''` or `'': ""`, so convert `"""` -> `"`, `'''` -> `'` - output = output.replace('"""', '"').replace("'''", '"') - arr = output.split("\n") - rline = arr[line_no] # raw line - line = arr[line_no].strip() - # different general problems - if line.endswith("],"): - # problem, redundant char `]` - new_line = line.replace("]", "") - elif line.endswith("},") and not output.endswith("},"): - # problem, redundant char `}` - new_line = line.replace("}", "") - elif line.endswith("},") and output.endswith("},"): - new_line = line[:-1] - # remove comments in output json str, after json value content, maybe start with #, maybe start with // - elif rline[col_no] == "#" or rline[col_no] == "/": - new_line = rline[:col_no] - for i in range(line_no + 1, len(arr)): - # look for # or // comments and make sure they are not inside the string value - comment_index = -1 - for match in re.finditer(r"(\".*?\"|\'.*?\')|(#|//)", line): - if match.group(1): # if the string value - continue - if match.group(2): # if comments - comment_index = match.start(2) - break - # if comments, then delete them - if comment_index != -1: - arr[i] = arr[i][:comment_index].rstrip() - elif (rline[col_no] in ["'", '"']) and (line.startswith('"') or line.startswith("'")) and "," not in line: - # problem, `"""` or `'''` without `,` - new_line = f",{line}" - elif '",' not in line and "," not in line and '"' not in line: - new_line = f'{line}",' - elif not line.endswith(","): - # problem, miss char `,` at the end. - new_line = f"{line}," - elif "," in line and len(line) == 1: - new_line = f'"{line}' - elif '",' in line: - new_line = line[:-2] + "'," - else: - new_line = line - - arr[line_no] = new_line - output = "\n".join(arr) - logger.info(f"repair_invalid_json, raw error: {error}") - - return output - - -def run_after_exp_and_passon_next_retry(logger: "loguru.Logger") -> Callable[["RetryCallState"], None]: - def run_and_passon(retry_state: RetryCallState) -> None: - """ - RetryCallState example - { - "start_time":143.098322024, - "retry_object":")>", - "fn":"", - "args":"(\"tag:[/CONTENT]\",)", # function input args - "kwargs":{}, # function input kwargs - "attempt_number":1, # retry number - "outcome":"", # type(outcome.result()) = "str", type(outcome.exception()) = "class" - "outcome_timestamp":143.098416904, - "idle_for":0, - "next_action":"None" - } - """ - if retry_state.outcome.failed: - if retry_state.args: - # # can't be used as args=retry_state.args - func_param_output = retry_state.args[0] - elif retry_state.kwargs: - func_param_output = retry_state.kwargs.get("output", "") - exp_str = str(retry_state.outcome.exception()) - - fix_str = "try to fix it, " if CONFIG.repair_llm_output else "" - logger.warning( - f"parse json from content inside [CONTENT][/CONTENT] failed at retry " - f"{retry_state.attempt_number}, {fix_str}exp: {exp_str}" - ) - - repaired_output = repair_invalid_json(func_param_output, exp_str) - retry_state.kwargs["output"] = repaired_output - - return run_and_passon - - -@retry( - stop=stop_after_attempt(3 if CONFIG.repair_llm_output else 0), - wait=wait_fixed(1), - after=run_after_exp_and_passon_next_retry(logger), -) -def retry_parse_json_text(output: str) -> Union[list, dict]: - """ - repair the json-text situation like there are extra chars like [']', '}'] - - Warning - if CONFIG.repair_llm_output is False, retry _aask_v1 {x=3} times, and the retry_parse_json_text's retry not work - 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}") - - # if CONFIG.repair_llm_output is True, it will try to fix output until the retry break - parsed_data = CustomDecoder(strict=False).decode(output) - - return parsed_data - - -def extract_content_from_output(content: str, right_key: str = "[/CONTENT]"): - """extract xxx from [CONTENT](xxx)[/CONTENT] using regex pattern""" - - def re_extract_content(cont: str, pattern: str) -> str: - matches = re.findall(pattern, cont, re.DOTALL) - for match in matches: - if match: - cont = match - break - return cont.strip() - - # TODO construct the extract pattern with the `right_key` - raw_content = copy.deepcopy(content) - pattern = r"\[CONTENT\]([\s\S]*)\[/CONTENT\]" - new_content = re_extract_content(raw_content, pattern) - - if not new_content.startswith("{"): - # TODO find a more general pattern - # # for `[CONTENT]xxx[CONTENT]xxxx[/CONTENT] situation - logger.warning(f"extract_content try another pattern: {pattern}") - if right_key not in new_content: - raw_content = copy.deepcopy(new_content + "\n" + right_key) - # # pattern = r"\[CONTENT\](\s*\{.*?\}\s*)\[/CONTENT\]" - new_content = re_extract_content(raw_content, pattern) - else: - if right_key in new_content: - idx = new_content.find(right_key) - new_content = new_content[:idx] - new_content = new_content.strip() - - return new_content - - -def extract_state_value_from_output(content: str) -> str: - """ - For openai models, they will always return state number. But for open llm models, the instruction result maybe a - long text contain target number, so here add a extraction to improve success rate. - - Args: - content (str): llm's output from `Role._think` - """ - content = content.strip() # deal the output cases like " 0", "0\n" and so on. - pattern = r"([0-9])" # TODO find the number using a more proper method not just extract from content using pattern - matches = re.findall(pattern, content, re.DOTALL) - matches = list(set(matches)) - state = matches[0] if len(matches) > 0 else "-1" - return state From 11f70ca9b1714dc312a847505c9940f0c60a24b1 Mon Sep 17 00:00:00 2001 From: huzixia <528543747@qq.com> Date: Sat, 27 Jan 2024 18:06:52 +0800 Subject: [PATCH 07/27] modify code based on feedback of action_node.py and repair_llm_raw_output.py, add code in test_repair_llm_raw_output.py --- metagpt/actions/action_node.py | 5 +--- metagpt/utils/repair_llm_raw_output.py | 9 +------ .../utils/test_repair_llm_raw_output.py | 26 +++++++++++++++++++ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/metagpt/actions/action_node.py b/metagpt/actions/action_node.py index ed0e27869..6c65b33ef 100644 --- a/metagpt/actions/action_node.py +++ b/metagpt/actions/action_node.py @@ -23,10 +23,7 @@ from metagpt.utils.common import OutputParser, general_after_log TAG = "CONTENT" LANGUAGE_CONSTRAINT = "Language: Please use the same language as Human INPUT." -FORMAT_CONSTRAINT = (f"Format: output wrapped inside [{TAG}][/{TAG}] like format example, nothing else. " - f"Delete comments in json") -# Delete comments in json -# If you don't want JSONDecodeError to occur, you can add Delete comments in json after FORMAT_CONSTRAINT +FORMAT_CONSTRAINT = f"Format: output wrapped inside [{TAG}][/{TAG}] like format example, nothing else." SIMPLE_TEMPLATE = """ diff --git a/metagpt/utils/repair_llm_raw_output.py b/metagpt/utils/repair_llm_raw_output.py index ef3580750..973cffb8a 100644 --- a/metagpt/utils/repair_llm_raw_output.py +++ b/metagpt/utils/repair_llm_raw_output.py @@ -137,11 +137,10 @@ def repair_json_format(output: str) -> str: elif output.startswith("{") and output.endswith("]"): output = output[:-1] + "}" - # remove comments in output json str, after json value content, maybe start with #, maybe start with // + # remove comments in output json string arr = output.split("\n") new_arr = [] for line in arr: - # look for # or // comments and make sure they are not inside the string value new_line = remove_comments_from_line(line) new_arr.append(new_line) output = "\n".join(new_arr) @@ -214,12 +213,6 @@ def repair_invalid_json(output: str, error: str) -> str: new_line = line.replace("}", "") elif line.endswith("},") and output.endswith("},"): new_line = line[:-1] - # remove comments in output json str, after json value content, maybe start with #, maybe start with // - elif rline[col_no] == "#" or rline[col_no] == "/": - new_line = rline[:col_no] - # check the next line and remove the comments - for i in range(line_no + 1, len(arr)): - arr[i] = remove_comments_from_line(arr[i]) elif (rline[col_no] in ["'", '"']) and (line.startswith('"') or line.startswith("'")) and "," not in line: # problem, `"""` or `'''` without `,` new_line = f",{line}" diff --git a/tests/metagpt/utils/test_repair_llm_raw_output.py b/tests/metagpt/utils/test_repair_llm_raw_output.py index 1f809a081..9d53b8243 100644 --- a/tests/metagpt/utils/test_repair_llm_raw_output.py +++ b/tests/metagpt/utils/test_repair_llm_raw_output.py @@ -141,6 +141,32 @@ def test_repair_json_format(): output = repair_llm_raw_output(output=raw_output, req_keys=[None], repair_type=RepairType.JSON) assert output == target_output + raw_output = """ +{ + "Language": "en_us", // define language + "Programming Language": "Python" # define code language +} +""" + target_output = """{ + "Language": "en_us", + "Programming Language": "Python" +}""" + output = repair_llm_raw_output(output=raw_output, req_keys=[None], repair_type=RepairType.JSON) + assert output == target_output + + raw_output = """ + { + "Language": "#en_us#", // define language + "Programming Language": "//Python # Code // Language//" # define code language + } + """ + target_output = """{ + "Language": "#en_us#", + "Programming Language": "//Python # Code // Language//" + }""" + output = repair_llm_raw_output(output=raw_output, req_keys=[None], repair_type=RepairType.JSON) + assert output == target_output + def test_repair_invalid_json(): from metagpt.utils.repair_llm_raw_output import repair_invalid_json From c3b4c698d80cba70e446cd6a97f375459c8c5595 Mon Sep 17 00:00:00 2001 From: huzixia <528543747@qq.com> Date: Sat, 27 Jan 2024 18:23:57 +0800 Subject: [PATCH 08/27] update repair_llm_raw_output.py --- metagpt/utils/repair_llm_raw_output.py | 36 ++++++++++---------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/metagpt/utils/repair_llm_raw_output.py b/metagpt/utils/repair_llm_raw_output.py index 973cffb8a..6da974d96 100644 --- a/metagpt/utils/repair_llm_raw_output.py +++ b/metagpt/utils/repair_llm_raw_output.py @@ -105,23 +105,6 @@ def repair_required_key_pair_missing(output: str, req_key: str = "[/CONTENT]") - return output -def remove_comments_from_line(line): - """ - Remove comments from a single line of string. - Comments are assumed to start with '#' or '//' and are not inside string values. - """ - comment_index = -1 - for match in re.finditer(r"(\".*?\"|\'.*?\')|(#|//)", line): - if match.group(1): # if the string value - continue - if match.group(2): # if comments - comment_index = match.start(2) - break - if comment_index != -1: # if comments, then delete them - return line[:comment_index].rstrip() - return line - - def repair_json_format(output: str) -> str: """ fix extra `[` or `}` in the end @@ -136,13 +119,22 @@ def repair_json_format(output: str) -> str: logger.info(f"repair_json_format: {'}]'}") elif output.startswith("{") and output.endswith("]"): output = output[:-1] + "}" - - # remove comments in output json string + # remove comments in output json string, after json value content, maybe start with #, maybe start with // arr = output.split("\n") new_arr = [] - for line in arr: - new_line = remove_comments_from_line(line) - new_arr.append(new_line) + for json_line in arr: + # look for # or // comments and make sure they are not inside the string value + comment_index = -1 + for match in re.finditer(r"(\".*?\"|\'.*?\')|(#|//)", json_line): + if match.group(1): # if the string value + continue + if match.group(2): # if comments + comment_index = match.start(2) + break + # if comments, then delete them + if comment_index != -1: + json_line = json_line[:comment_index].rstrip() + new_arr.append(json_line) output = "\n".join(new_arr) return output From a68c3442bcff09864409ed47a02bbcc476657d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=85=81=E6=9D=83?= Date: Sun, 28 Jan 2024 10:39:00 +0800 Subject: [PATCH 09/27] Refactor get_choice_delta_text for safer dict access --- metagpt/provider/base_llm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metagpt/provider/base_llm.py b/metagpt/provider/base_llm.py index a50cdacd9..47c527b97 100644 --- a/metagpt/provider/base_llm.py +++ b/metagpt/provider/base_llm.py @@ -91,7 +91,7 @@ class BaseLLM(ABC): def get_choice_delta_text(self, rsp: dict) -> str: """Required to provide the first text of stream choice""" - return rsp.get("choices")[0]["delta"]["content"] + return rsp.get("choices", [{}])[0].get("delta", {}).get("content", "") def get_choice_function(self, rsp: dict) -> dict: """Required to provide the first function of choice From f4c6e507c9d21ea2375a87f12d83a885838e2e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=98=E6=9D=83=20=E9=A9=AC?= Date: Wed, 31 Jan 2024 22:44:01 +0800 Subject: [PATCH 10/27] fixbug: make unit test stable --- tests/metagpt/tools/test_ut_writer.py | 49 ++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/tests/metagpt/tools/test_ut_writer.py b/tests/metagpt/tools/test_ut_writer.py index 29b6572c2..3cc7e86bb 100644 --- a/tests/metagpt/tools/test_ut_writer.py +++ b/tests/metagpt/tools/test_ut_writer.py @@ -8,6 +8,17 @@ from pathlib import Path import pytest +from openai.resources.chat.completions import AsyncCompletions +from openai.types import CompletionUsage +from openai.types.chat.chat_completion import ( + ChatCompletion, + ChatCompletionMessage, + Choice, +) +from openai.types.chat.chat_completion_message_tool_call import ( + ChatCompletionMessageToolCall, + Function, +) from metagpt.config2 import config from metagpt.const import API_QUESTIONS_PATH, UT_PY_PATH @@ -16,7 +27,43 @@ from metagpt.tools.ut_writer import YFT_PROMPT_PREFIX, UTGenerator class TestUTWriter: @pytest.mark.asyncio - async def test_api_to_ut_sample(self): + async def test_api_to_ut_sample(self, mocker): + async def mock_create(*args, **kwargs): + return ChatCompletion( + id="chatcmpl-8n5fAd21w2J1IIFkI4qxWlNfM7QRC", + choices=[ + Choice( + finish_reason="stop", + index=0, + logprobs=None, + message=ChatCompletionMessage( + content=None, + role="assistant", + function_call=None, + tool_calls=[ + ChatCompletionMessageToolCall( + id="call_EjjmIY7GMspHu3r9mx8gPA2k", + function=Function( + arguments='{"code":"import string\\nimport random\\n\\ndef random_string' + "(length=10):\\n return ''.join(random.choice(string.ascii_" + 'lowercase) for i in range(length))"}', + name="execute", + ), + type="function", + ) + ], + ), + ) + ], + created=1706710532, + model="gpt-3.5-turbo-1106", + object="chat.completion", + system_fingerprint="fp_04f9a1eebf", + usage=CompletionUsage(completion_tokens=35, prompt_tokens=1982, total_tokens=2017), + ) + + mocker.patch.object(AsyncCompletions, "create", mock_create) + # Prerequisites swagger_file = Path(__file__).parent / "../../data/ut_writer/yft_swaggerApi.json" assert swagger_file.exists() From 90f84ad452876ff9b8cf2def0c08faa20925b805 Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 1 Feb 2024 10:19:40 +0800 Subject: [PATCH 11/27] Update FAQ-EN.md --- docs/FAQ-EN.md | 174 +++++++++++++++---------------------------------- 1 file changed, 53 insertions(+), 121 deletions(-) diff --git a/docs/FAQ-EN.md b/docs/FAQ-EN.md index 88b5b0573..d3caa244e 100644 --- a/docs/FAQ-EN.md +++ b/docs/FAQ-EN.md @@ -1,161 +1,93 @@ Our vision is to [extend human life](https://github.com/geekan/HowToLiveLonger) and [reduce working hours](https://github.com/geekan/MetaGPT/). -1. ### Convenient Link for Sharing this Document: +### Convenient Link for Sharing this Document: ``` -- MetaGPT-Index/FAQ https://deepwisdom.feishu.cn/wiki/MsGnwQBjiif9c3koSJNcYaoSnu4 +- MetaGPT-Index/FAQ-EN https://github.com/geekan/MetaGPT/blob/main/docs/FAQ-EN.md +- MetaGPT-Index/FAQ-CN https://deepwisdom.feishu.cn/wiki/MsGnwQBjiif9c3koSJNcYaoSnu4 ``` -2. ### Link - - +### Link 1. Code:https://github.com/geekan/MetaGPT - -1. Roadmap:https://github.com/geekan/MetaGPT/blob/main/docs/ROADMAP.md - -1. EN - - 1. Demo Video: [MetaGPT: Multi-Agent AI Programming Framework](https://www.youtube.com/watch?v=8RNzxZBTW8M) +2. Roadmap:https://github.com/geekan/MetaGPT/blob/main/docs/ROADMAP.md +3. EN + 1. Demo Video: [MetaGPT: Multi-Agent AI Programming Framework](https://www.youtube.com/watch?v=8RNzxZBTW8M) 2. Tutorial: [MetaGPT: Deploy POWERFUL Autonomous Ai Agents BETTER Than SUPERAGI!](https://www.youtube.com/watch?v=q16Gi9pTG_M&t=659s) 3. Author's thoughts video(EN): [MetaGPT Matthew Berman](https://youtu.be/uT75J_KG_aY?si=EgbfQNAwD8F5Y1Ak) +4. CN + 1. Demo Video: [MetaGPT:一行代码搭建你的虚拟公司_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1NP411C7GW/?spm_id_from=333.999.0.0&vd_source=735773c218b47da1b4bd1b98a33c5c77) + 1. Tutorial: [一个提示词写游戏 Flappy bird, 比AutoGPT强10倍的MetaGPT,最接近AGI的AI项目](https://youtu.be/Bp95b8yIH5c) + 2. Author's thoughts video(CN): [MetaGPT作者深度解析直播回放_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1Ru411V7XL/?spm_id_from=333.337.search-card.all.click) -1. CN - - 1. Demo Video: [MetaGPT:一行代码搭建你的虚拟公司_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1NP411C7GW/?spm_id_from=333.999.0.0&vd_source=735773c218b47da1b4bd1b98a33c5c77) - 1. Tutorial: [一个提示词写游戏 Flappy bird, 比AutoGPT强10倍的MetaGPT,最接近AGI的AI项目](https://youtu.be/Bp95b8yIH5c) - 2. Author's thoughts video(CN): [MetaGPT作者深度解析直播回放_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1Ru411V7XL/?spm_id_from=333.337.search-card.all.click) - - - -3. ### How to become a contributor? - - +### How to become a contributor? 1. Choose a task from the Roadmap (or you can propose one). By submitting a PR, you can become a contributor and join the dev team. -1. Current contributors come from backgrounds including ByteDance AI Lab/DingDong/Didi/Xiaohongshu, Tencent/Baidu/MSRA/TikTok/BloomGPT Infra/Bilibili/CUHK/HKUST/CMU/UCB +2. Current contributors come from backgrounds including ByteDance AI Lab/DingDong/Didi/Xiaohongshu, Tencent/Baidu/MSRA/TikTok/BloomGPT Infra/Bilibili/CUHK/HKUST/CMU/UCB - - -4. ### Chief Evangelist (Monthly Rotation) +### Chief Evangelist (Monthly Rotation) MetaGPT Community - The position of Chief Evangelist rotates on a monthly basis. The primary responsibilities include: 1. Maintaining community FAQ documents, announcements, and Github resources/READMEs. -1. Responding to, answering, and distributing community questions within an average of 30 minutes, including on platforms like Github Issues, Discord and WeChat. -1. Upholding a community atmosphere that is enthusiastic, genuine, and friendly. -1. Encouraging everyone to become contributors and participate in projects that are closely related to achieving AGI (Artificial General Intelligence). -1. (Optional) Organizing small-scale events, such as hackathons. +2. Responding to, answering, and distributing community questions within an average of 30 minutes, including on platforms like Github Issues, Discord and WeChat. +3. Upholding a community atmosphere that is enthusiastic, genuine, and friendly. +4. Encouraging everyone to become contributors and participate in projects that are closely related to achieving AGI (Artificial General Intelligence). +5. (Optional) Organizing small-scale events, such as hackathons. - - -5. ### FAQ - - - -1. Experience with the generated repo code: - - 1. https://github.com/geekan/MetaGPT/releases/tag/v0.1.0 +### FAQ 1. Code truncation/ Parsing failure: - - 1. Check if it's due to exceeding length. Consider using the gpt-3.5-turbo-16k or other long token versions. - -1. Success rate: - - 1. There hasn't been a quantitative analysis yet, but the success rate of code generated by GPT-4 is significantly higher than that of gpt-3.5-turbo. - -1. Support for incremental, differential updates (if you wish to continue a half-done task): - - 1. Several prerequisite tasks are listed on the ROADMAP. - -1. Can existing code be loaded? - - 1. It's not on the ROADMAP yet, but there are plans in place. It just requires some time. - -1. Support for multiple programming languages and natural languages? - - 1. It's listed on ROADMAP. - -1. Want to join the contributor team? How to proceed? - + 1. Check if it's due to exceeding length. Consider using the gpt-4-turbo-preview or other long token versions. +2. Success rate: + 1. There hasn't been a quantitative analysis yet, but the success rate of code generated by gpt-4-turbo-preview is significantly higher than that of gpt-3.5-turbo. +3. Support for incremental, differential updates (if you wish to continue a half-done task): + 1. There is now an experimental version. Specify `--inc --project-path ""` or `--inc --project-name ""` on the command line and enter the corresponding requirements to try it. +4. Can existing code be loaded? + 1. We are doing this, but it is very difficult, especially when the project is large, it is very difficult to achieve a high success rate. +5. Support for multiple programming languages and natural languages? + 1. It is now supported, but it is still in experimental version +6. Want to join the contributor team? How to proceed? 1. Merging a PR will get you into the contributor's team. The main ongoing tasks are all listed on the ROADMAP. - -1. PRD stuck / unable to access/ connection interrupted - +7. PRD stuck / unable to access/ connection interrupted 1. The official openai base_url address is `https://api.openai.com/v1` - 1. If the official openai base_url address is inaccessible in your environment (this can be verified with curl), it's recommended to configure using the reverse proxy openai base_url provided by libraries such as openai-forward. For instance, `openai base_url: "``https://api.openai-forward.com/v1``"` - 1. If the official openai base_url address is inaccessible in your environment (again, verifiable via curl), another option is to configure the llm.proxy parameter. This way, you can access the official openai base_url via a local proxy. If you don't need to access via a proxy, please do not enable this configuration; if accessing through a proxy is required, modify it to the correct proxy address. Note that when llm.proxy is enabled, don't set openai base_url. - 1. Note: OpenAI's default API design ends with a v1. An example of the correct configuration is: `openai base_url: "``https://api.openai.com/v1``"` - -1. Absolutely! How can I assist you today? - + 2. If the official openai base_url address is inaccessible in your environment (this can be verified with curl), it's recommended to configure using base_url to other "reverse-proxy" provider such as openai-forward. For instance, `openai base_url: "``https://api.openai-forward.com/v1``"` + 3. If the official openai base_url address is inaccessible in your environment (again, verifiable via curl), another option is to configure the llm.proxy in the `config2.yaml`. This way, you can access the official openai base_url via a local proxy. If you don't need to access via a proxy, please do not enable this configuration; if accessing through a proxy is required, modify it to the correct proxy address. + 4. Note: OpenAI's default API design ends with a v1. An example of the correct configuration is: `base_url: "https://api.openai.com/v1" +8. Get reply: "Absolutely! How can I assist you today?" 1. Did you use Chi or a similar service? These services are prone to errors, and it seems that the error rate is higher when consuming 3.5k-4k tokens in GPT-4 - -1. What does Max token mean? - +9. What does Max token mean? 1. It's a configuration for OpenAI's maximum response length. If the response exceeds the max token, it will be truncated. - -1. How to change the investment amount? - +10. How to change the investment amount? 1. You can view all commands by typing `metagpt --help` - -1. Which version of Python is more stable? - +11. Which version of Python is more stable? 1. python3.9 / python3.10 - -1. Can't use GPT-4, getting the error "The model gpt-4 does not exist." - +12. Can't use GPT-4, getting the error "The model gpt-4 does not exist." 1. OpenAI's official requirement: You can use GPT-4 only after spending $1 on OpenAI. 1. Tip: Run some data with gpt-3.5-turbo (consume the free quota and $1), and then you should be able to use gpt-4. - -1. Can games whose code has never been seen before be written? - +13. Can games whose code has never been seen before be written? 1. Refer to the README. The recommendation system of Toutiao is one of the most complex systems in the world currently. Although it's not on GitHub, many discussions about it exist online. If it can visualize these, it suggests it can also summarize these discussions and convert them into code. The prompt would be something like "write a recommendation system similar to Toutiao". Note: this was approached in earlier versions of the software. The SOP of those versions was different; the current one adopts Elon Musk's five-step work method, emphasizing trimming down requirements as much as possible. - -1. Under what circumstances would there typically be errors? - +14. Under what circumstances would there typically be errors? 1. More than 500 lines of code: some function implementations may be left blank. - 1. When using a database, it often gets the implementation wrong — since the SQL database initialization process is usually not in the code. - 1. With more lines of code, there's a higher chance of false impressions, leading to calls to non-existent APIs. - -1. An error occurred during installation: "Another program is using this file...egg". - + 2. When using a database, it often gets the implementation wrong — since the SQL database initialization process is usually not in the code. + 3. With more lines of code, there's a higher chance of false impressions, leading to calls to non-existent APIs. +15. An error occurred during installation: "Another program is using this file...egg". 1. Delete the file and try again. - 1. Or manually execute`pip install -r requirements.txt` - -1. The origin of the name MetaGPT? - + 2. Or manually execute`pip install -r requirements.txt` +16. The origin of the name MetaGPT? 1. The name was derived after iterating with GPT-4 over a dozen rounds. GPT-4 scored and suggested it. - -1. Is there a more step-by-step installation tutorial? - - 1. Youtube(CN):[一个提示词写游戏 Flappy bird, 比AutoGPT强10倍的MetaGPT,最接近AGI的AI项目=一个软件公司产品经理+程序员](https://youtu.be/Bp95b8yIH5c) - 1. Youtube(EN)https://www.youtube.com/watch?v=q16Gi9pTG_M&t=659s - 2. video(EN): [MetaGPT Matthew Berman](https://youtu.be/uT75J_KG_aY?si=EgbfQNAwD8F5Y1Ak) - -1. openai.error.RateLimitError: You exceeded your current quota, please check your plan and billing details - +17. openai.error.RateLimitError: You exceeded your current quota, please check your plan and billing details 1. If you haven't exhausted your free quota, set RPM to 3 or lower in the settings. - 1. If your free quota is used up, consider adding funds to your account. - -1. What does "borg" mean in n_borg? - + 2. If your free quota is used up, consider adding funds to your account. +18. What does "borg" mean in n_borg? 1. [Wikipedia borg meaning ](https://en.wikipedia.org/wiki/Borg) - 1. The Borg civilization operates based on a hive or collective mentality, known as "the Collective." Every Borg individual is connected to the collective via a sophisticated subspace network, ensuring continuous oversight and guidance for every member. This collective consciousness allows them to not only "share the same thoughts" but also to adapt swiftly to new strategies. While individual members of the collective rarely communicate, the collective "voice" sometimes transmits aboard ships. - -1. How to use the Claude API? - + 2. The Borg civilization operates based on a hive or collective mentality, known as "the Collective." Every Borg individual is connected to the collective via a sophisticated subspace network, ensuring continuous oversight and guidance for every member. This collective consciousness allows them to not only "share the same thoughts" but also to adapt swiftly to new strategies. While individual members of the collective rarely communicate, the collective "voice" sometimes transmits aboard ships. +19. How to use the Claude API? 1. The full implementation of the Claude API is not provided in the current code. 1. You can use the Claude API through third-party API conversion projects like: https://github.com/jtsang4/claude-to-chatgpt - -1. Is Llama2 supported? - +20. Is Llama2 supported? 1. On the day Llama2 was released, some of the community members began experiments and found that output can be generated based on MetaGPT's structure. However, Llama2's context is too short to generate a complete project. Before regularly using Llama2, it's necessary to expand the context window to at least 8k. If anyone has good recommendations for expansion models or methods, please leave a comment. - -1. `mermaid-cli getElementsByTagName SyntaxError: Unexpected token '.'` - +21. `mermaid-cli getElementsByTagName SyntaxError: Unexpected token '.'` 1. Upgrade node to version 14.x or above: - 1. `npm install -g n` - 1. `n stable` to install the stable version of node(v18.x) + 2. `n stable` to install the stable version of node(v18.x) From 026dd8167cc0ae95a1eddb8a2a74b97c2518e2e1 Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 1 Feb 2024 10:21:58 +0800 Subject: [PATCH 12/27] Update cli_install.md --- docs/install/cli_install.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/install/cli_install.md b/docs/install/cli_install.md index 33d759758..ab360fad2 100644 --- a/docs/install/cli_install.md +++ b/docs/install/cli_install.md @@ -33,11 +33,12 @@ # Step 3: Clone the repository to your local machine, and install it. npm install @mermaid-js/mermaid-cli ``` -- don't forget to the configuration for mmdc in config.yml +- don't forget to the configuration for mmdc path in config.yml ```yml - puppeteer_config: "./config/puppeteer-config.json" - path: "./node_modules/.bin/mmdc" + mermaid: + puppeteer_config: "./config/puppeteer-config.json" + path: "./node_modules/.bin/mmdc" ``` - if `pip install -e.` fails with error `[Errno 13] Permission denied: '/usr/local/lib/python3.11/dist-packages/test-easy-install-13129.write-test'`, try instead running `pip install -e. --user` @@ -61,7 +62,7 @@ # Step 3: Clone the repository to your local machine, and install it. - **modify `config2.yaml`** - uncomment mermaid.engine from config2.yaml and change it to `playwright` + change mermaid.engine to `playwright` ```yaml mermaid: @@ -91,7 +92,7 @@ # Step 3: Clone the repository to your local machine, and install it. - **modify `config2.yaml`** - uncomment mermaid.engine from config2.yaml and change it to `pyppeteer` + change mermaid.engine to `pyppeteer` ```yaml mermaid: @@ -100,8 +101,8 @@ # Step 3: Clone the repository to your local machine, and install it. - mermaid.ink - **modify `config2.yaml`** - - uncomment mermaid.engine from config2.yaml and change it to `ink` + + change mermaid.engine to `ink` ```yaml mermaid: @@ -109,4 +110,4 @@ # Step 3: Clone the repository to your local machine, and install it. ``` Note: this method does not support pdf export. - \ No newline at end of file + From 3fa2b3216effaeabfcb8294bbc674e2bb9becbc7 Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 1 Feb 2024 10:35:51 +0800 Subject: [PATCH 13/27] refine docs --- docs/install/cli_install.md | 28 +++++++++++++------ docs/install/cli_install_cn.md | 33 ++++++++++++++++------- docs/tutorial/usage.md | 49 +++++++++++++++++----------------- docs/tutorial/usage_cn.md | 49 +++++++++++++++++----------------- 4 files changed, 91 insertions(+), 68 deletions(-) diff --git a/docs/install/cli_install.md b/docs/install/cli_install.md index ab360fad2..b79ad9cb7 100644 --- a/docs/install/cli_install.md +++ b/docs/install/cli_install.md @@ -9,17 +9,29 @@ ### Support System and version ### Detail Installation ```bash -# Step 1: Ensure that NPM is installed on your system. Then install mermaid-js. (If you don't have npm in your computer, please go to the Node.js official website to install Node.js https://nodejs.org/ and then you will have npm tool in your computer.) -npm --version -sudo npm install -g @mermaid-js/mermaid-cli - -# Step 2: Ensure that Python 3.9+ is installed on your system. You can check this by using: +# Step 1: Ensure that Python 3.9+ is installed on your system. You can check this by using: +# You can use conda to initialize a new python env +# conda create -n metagpt python=3.9 +# conda activate metagpt python3 --version -# Step 3: Clone the repository to your local machine, and install it. +# Step 2: Clone the repository to your local machine for latest version, and install it. git clone https://github.com/geekan/MetaGPT.git cd MetaGPT -pip install -e. +pip3 install -e . # or pip3 install metagpt # for stable version + +# Step 3: setup your LLM key in the config2.yaml file +mkdir ~/.metagpt +cp config/config2.yaml ~/.metagpt/config2.yaml +vim ~/.metagpt/config2.yaml + +# Step 4: run metagpt cli +metagpt "Create a 2048 game in python" + +# Step 5 [Optional]: If you want to save the artifacts like diagrams such as quadrant chart, system designs, sequence flow in the workspace, you can execute the step before Step 3. By default, the framework is compatible, and the entire process can be run completely without executing this step. +# If executing, ensure that NPM is installed on your system. Then install mermaid-js. (If you don't have npm in your computer, please go to the Node.js official website to install Node.js https://nodejs.org/ and then you will have npm tool in your computer.) +npm --version +sudo npm install -g @mermaid-js/mermaid-cli ``` **Note:** @@ -35,7 +47,7 @@ # Step 3: Clone the repository to your local machine, and install it. - don't forget to the configuration for mmdc path in config.yml - ```yml + ```yaml mermaid: puppeteer_config: "./config/puppeteer-config.json" path: "./node_modules/.bin/mmdc" diff --git a/docs/install/cli_install_cn.md b/docs/install/cli_install_cn.md index 891b72d24..1ee18d9a6 100644 --- a/docs/install/cli_install_cn.md +++ b/docs/install/cli_install_cn.md @@ -10,17 +10,29 @@ ### 支持的系统和版本 ### 详细安装 ```bash -# 第 1 步:确保您的系统上安装了 NPM。并使用npm安装mermaid-js -npm --version -sudo npm install -g @mermaid-js/mermaid-cli - -# 第 2 步:确保您的系统上安装了 Python 3.9+。您可以使用以下命令进行检查: +# 步骤 1: 确保您的系统安装了 Python 3.9 或更高版本。您可以使用以下命令来检查: +# 您可以使用 conda 来初始化一个新的 Python 环境 +# conda create -n metagpt python=3.9 +# conda activate metagpt python3 --version -# 第 3 步:克隆仓库到您的本地机器,并进行安装。 +# 步骤 2: 克隆仓库到您的本地机器以获取最新版本,并安装它。 git clone https://github.com/geekan/MetaGPT.git cd MetaGPT -pip install -e. +pip3 install -e . # 或 pip3 install metagpt # 用于稳定版本 + +# 步骤 3: 在 config2.yaml 文件中设置您的 LLM 密钥 +mkdir ~/.metagpt +cp config/config2.yaml ~/.metagpt/config2.yaml +vim ~/.metagpt/config2.yaml + +# 步骤 4: 运行 metagpt 命令行界面 +metagpt "用 python 创建一个 2048 游戏" + +# 步骤 5 [可选]: 如果您想保存诸如象限图、系统设计、序列流等图表作为工作空间的工件,您可以在执行步骤 3 之前执行此步骤。默认情况下,该框架是兼容的,整个过程可以完全不执行此步骤而运行。 +# 如果执行此步骤,请确保您的系统上安装了 NPM。然后安装 mermaid-js。(如果您的计算机中没有 npm,请访问 Node.js 官方网站 https://nodejs.org/ 安装 Node.js,然后您将在计算机中拥有 npm 工具。) +npm --version +sudo npm install -g @mermaid-js/mermaid-cli ``` **注意:** @@ -33,11 +45,12 @@ # 第 3 步:克隆仓库到您的本地机器,并进行安装。 npm install @mermaid-js/mermaid-cli ``` -- 不要忘记在config.yml中为mmdc配置配置, +- 不要忘记在config.yml中为mmdc配置 ```yml - puppeteer_config: "./config/puppeteer-config.json" - path: "./node_modules/.bin/mmdc" + mermaid: + puppeteer_config: "./config/puppeteer-config.json" + path: "./node_modules/.bin/mmdc" ``` - 如果`pip install -e.`失败并显示错误`[Errno 13] Permission denied: '/usr/local/lib/python3.11/dist-packages/test-easy-install-13129.write-test'`,请尝试使用`pip install -e. --user`运行。 diff --git a/docs/tutorial/usage.md b/docs/tutorial/usage.md index 809f91e1f..e8bfc37d9 100644 --- a/docs/tutorial/usage.md +++ b/docs/tutorial/usage.md @@ -34,29 +34,28 @@ ### Preference of Platform or Tool ### Usage ``` -NAME - metagpt - We are a software startup comprised of AI. By investing in us, you are empowering a future filled with limitless possibilities. - -SYNOPSIS - metagpt IDEA - -DESCRIPTION - We are a software startup comprised of AI. By investing in us, you are empowering a future filled with limitless possibilities. - -POSITIONAL ARGUMENTS - IDEA - Type: str - Your innovative idea, such as "Creating a snake game." - -FLAGS - --investment=INVESTMENT - Type: float - Default: 3.0 - As an investor, you have the opportunity to contribute a certain dollar amount to this AI company. - --n_round=N_ROUND - Type: int - Default: 5 - -NOTES - You can also use flags syntax for POSITIONAL ARGUMENTS + Usage: metagpt [OPTIONS] [IDEA] + + Start a new project. + +╭─ Arguments ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ idea [IDEA] Your innovative idea, such as 'Create a 2048 game.' [default: None] │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Options ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ --investment FLOAT Dollar amount to invest in the AI company. [default: 3.0] │ +│ --n-round INTEGER Number of rounds for the simulation. [default: 5] │ +│ --code-review --no-code-review Whether to use code review. [default: code-review] │ +│ --run-tests --no-run-tests Whether to enable QA for adding & running tests. [default: no-run-tests] │ +│ --implement --no-implement Enable or disable code implementation. [default: implement] │ +│ --project-name TEXT Unique project name, such as 'game_2048'. │ +│ --inc --no-inc Incremental mode. Use it to coop with existing repo. [default: no-inc] │ +│ --project-path TEXT Specify the directory path of the old version project to fulfill the incremental requirements. │ +│ --reqa-file TEXT Specify the source file name for rewriting the quality assurance code. │ +│ --max-auto-summarize-code INTEGER The maximum number of times the 'SummarizeCode' action is automatically invoked, with -1 indicating unlimited. This parameter is used for debugging the │ +│ workflow. │ +│ [default: 0] │ +│ --recover-path TEXT recover the project from existing serialized storage [default: None] │ +│ --init-config --no-init-config Initialize the configuration file for MetaGPT. [default: no-init-config] │ +│ --help Show this message and exit. │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ``` \ No newline at end of file diff --git a/docs/tutorial/usage_cn.md b/docs/tutorial/usage_cn.md index 709ec9968..075e928fd 100644 --- a/docs/tutorial/usage_cn.md +++ b/docs/tutorial/usage_cn.md @@ -30,29 +30,28 @@ ### 平台或工具的倾向性 ### 使用 ``` -名称 - metagpt - 我们是一家AI软件创业公司。通过投资我们,您将赋能一个充满无限可能的未来。 - -概要 - metagpt IDEA - -描述 - 我们是一家AI软件创业公司。通过投资我们,您将赋能一个充满无限可能的未来。 - -位置参数 - IDEA - 类型: str - 您的创新想法,例如"写一个命令行贪吃蛇。" - -标志 - --investment=INVESTMENT - 类型: float - 默认值: 3.0 - 作为投资者,您有机会向这家AI公司投入一定的美元金额。 - --n_round=N_ROUND - 类型: int - 默认值: 5 - -备注 - 您也可以用`标志`的语法,来处理`位置参数` + Usage: metagpt [OPTIONS] [IDEA] + + Start a new project. + +╭─ Arguments ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ idea [IDEA] Your innovative idea, such as 'Create a 2048 game.' [default: None] │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Options ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ --investment FLOAT Dollar amount to invest in the AI company. [default: 3.0] │ +│ --n-round INTEGER Number of rounds for the simulation. [default: 5] │ +│ --code-review --no-code-review Whether to use code review. [default: code-review] │ +│ --run-tests --no-run-tests Whether to enable QA for adding & running tests. [default: no-run-tests] │ +│ --implement --no-implement Enable or disable code implementation. [default: implement] │ +│ --project-name TEXT Unique project name, such as 'game_2048'. │ +│ --inc --no-inc Incremental mode. Use it to coop with existing repo. [default: no-inc] │ +│ --project-path TEXT Specify the directory path of the old version project to fulfill the incremental requirements. │ +│ --reqa-file TEXT Specify the source file name for rewriting the quality assurance code. │ +│ --max-auto-summarize-code INTEGER The maximum number of times the 'SummarizeCode' action is automatically invoked, with -1 indicating unlimited. This parameter is used for debugging the │ +│ workflow. │ +│ [default: 0] │ +│ --recover-path TEXT recover the project from existing serialized storage [default: None] │ +│ --init-config --no-init-config Initialize the configuration file for MetaGPT. [default: no-init-config] │ +│ --help Show this message and exit. │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ``` From 7e973341682bdc555566aca47a534ef455f5346f Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 1 Feb 2024 10:53:08 +0800 Subject: [PATCH 14/27] refine docs --- README.md | 27 +++++++-------------------- docs/README_CN.md | 27 +++++++++------------------ docs/tutorial/usage.md | 2 +- docs/tutorial/usage_cn.md | 2 +- metagpt/startup.py | 22 +++++++++++----------- 5 files changed, 29 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 39dde8208..c8277d55e 100644 --- a/README.md +++ b/README.md @@ -55,30 +55,17 @@ ## Install ### Pip installation +> Ensure that Python 3.9+ is installed on your system. You can check this by using: `python --version`. + ```bash -# Step 1: Ensure that Python 3.9+ is installed on your system. You can check this by using: # You can use conda to initialize a new python env # conda create -n metagpt python=3.9 # conda activate metagpt -python3 --version +pip install metagpt +metagpt --init-config # this will create a ~/.metagpt/config2.yaml from config/config2.yaml, modify it to your own config -# Step 2: Clone the repository to your local machine for latest version, and install it. -git clone https://github.com/geekan/MetaGPT.git -cd MetaGPT -pip3 install -e . # or pip3 install metagpt # for stable version - -# Step 3: setup your LLM key in the config2.yaml file -mkdir ~/.metagpt -cp config/config2.yaml ~/.metagpt/config2.yaml -vim ~/.metagpt/config2.yaml - -# Step 4: run metagpt cli -metagpt "Create a 2048 game in python" - -# Step 5 [Optional]: If you want to save the artifacts like diagrams such as quadrant chart, system designs, sequence flow in the workspace, you can execute the step before Step 3. By default, the framework is compatible, and the entire process can be run completely without executing this step. -# If executing, ensure that NPM is installed on your system. Then install mermaid-js. (If you don't have npm in your computer, please go to the Node.js official website to install Node.js https://nodejs.org/ and then you will have npm tool in your computer.) -npm --version -sudo npm install -g @mermaid-js/mermaid-cli +# Usage: metagpt "" +metagpt "Create a 2048 game" ``` detail installation please refer to [cli_install](https://docs.deepwisdom.ai/main/en/guide/get_started/installation.html#install-stable-version) @@ -99,7 +86,7 @@ # Step 2: Run metagpt demo with container -v /opt/metagpt/config/config2.yaml:/app/metagpt/config/config2.yaml \ -v /opt/metagpt/workspace:/app/metagpt/workspace \ metagpt/metagpt:latest \ - metagpt "Write a cli snake game" + metagpt "Create a 2048 game" ``` detail installation please refer to [docker_install](https://docs.deepwisdom.ai/main/en/guide/get_started/installation.html#install-with-docker) diff --git a/docs/README_CN.md b/docs/README_CN.md index ebf5dd408..52e781560 100644 --- a/docs/README_CN.md +++ b/docs/README_CN.md @@ -35,29 +35,20 @@ # MetaGPT: 多智能体框架 ## 安装 ### Pip安装 +> 确保您的系统安装了 Python 3.9 或更高版本。您可以通过以下命令来检查:`python --version`。 + ```bash -# 第 1 步:确保您的系统上安装了 Python 3.9+。您可以使用以下命令进行检查: -# 可以使用conda来初始化新的python环境 +# 您可以使用 conda 来初始化一个新的 python 环境 # conda create -n metagpt python=3.9 # conda activate metagpt -python3 --version +pip install metagpt +metagpt --init-config # 这将会从 config/config2.yaml 创建一个 ~/.metagpt/config2.yaml,根据您的需求修改它 -# 第 2 步:克隆最新仓库到您的本地机器,并进行安装。 -git clone https://github.com/geekan/MetaGPT.git -cd MetaGPT -pip3 install -e. # 或者 pip3 install metagpt # 安装稳定版本 - -# 第 3 步:执行metagpt -# 拷贝config2.yaml为~/.metagpt/config2.yaml,并设置你自己的api_key -metagpt "Write a cli snake game" - -# 第 4 步【可选的】:如果你想在执行过程中保存像象限图、系统设计、序列流程等图表这些产物,可以在第3步前执行该步骤。默认的,框架做了兼容,在不执行该步的情况下,也可以完整跑完整个流程。 -# 如果执行,确保您的系统上安装了 NPM。并使用npm安装mermaid-js -npm --version -sudo npm install -g @mermaid-js/mermaid-cli +# 使用方法: metagpt "<创建一个游戏或软件>" +metagpt "创建一个 2048 游戏" ``` -详细的安装请安装 [cli_install](https://docs.deepwisdom.ai/guide/get_started/installation.html#install-stable-version) +详细的安装请参考 [cli_install](https://docs.deepwisdom.ai/guide/get_started/installation.html#install-stable-version) ### Docker安装 > 注意:在Windows中,你需要将 "/opt/metagpt" 替换为Docker具有创建权限的目录,比如"D:\Users\x\metagpt" @@ -78,7 +69,7 @@ # 步骤2: 使用容器运行metagpt演示 metagpt "Write a cli snake game" ``` -详细的安装请安装 [docker_install](https://docs.deepwisdom.ai/main/zh/guide/get_started/installation.html#%E4%BD%BF%E7%94%A8docker%E5%AE%89%E8%A3%85) +详细的安装请参考 [docker_install](https://docs.deepwisdom.ai/main/zh/guide/get_started/installation.html#%E4%BD%BF%E7%94%A8docker%E5%AE%89%E8%A3%85) ### 快速开始的演示视频 - 在 [MetaGPT Huggingface Space](https://huggingface.co/spaces/deepwisdom/MetaGPT) 上进行体验 diff --git a/docs/tutorial/usage.md b/docs/tutorial/usage.md index e8bfc37d9..1128e98a5 100644 --- a/docs/tutorial/usage.md +++ b/docs/tutorial/usage.md @@ -2,7 +2,7 @@ ## MetaGPT Usage ### Configuration -- Configure your `key` in any of `~/.metagpt/config2.yaml / config/config2.yaml` +- Configure your `api_key` in any of `~/.metagpt/config2.yaml / config/config2.yaml` - Priority order: `~/.metagpt/config2.yaml > config/config2.yaml` ```bash diff --git a/docs/tutorial/usage_cn.md b/docs/tutorial/usage_cn.md index 075e928fd..3b0c86279 100644 --- a/docs/tutorial/usage_cn.md +++ b/docs/tutorial/usage_cn.md @@ -2,7 +2,7 @@ ## MetaGPT 使用 ### 配置 -- 在 `~/.metagpt/config2.yaml / config/config2.yaml` 中配置您的 `key` +- 在 `~/.metagpt/config2.yaml / config/config2.yaml` 中配置您的 `api_key` - 优先级顺序:`~/.metagpt/config2.yaml > config/config2.yaml` ```bash diff --git a/metagpt/startup.py b/metagpt/startup.py index 4a077cab7..26bb29cd1 100644 --- a/metagpt/startup.py +++ b/metagpt/startup.py @@ -17,17 +17,17 @@ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False) def generate_repo( idea, - investment, - n_round, - code_review, - run_tests, - implement, - project_name, - inc, - project_path, - reqa_file, - max_auto_summarize_code, - recover_path, + investment=3.0, + n_round=5, + code_review=True, + run_tests=False, + implement=True, + project_name="", + inc=False, + project_path="", + reqa_file="", + max_auto_summarize_code=0, + recover_path=None, ) -> ProjectRepo: """Run the startup logic. Can be called from CLI or other Python scripts.""" from metagpt.roles import ( From c20aecf5f27c4a5fdc1340ecb0ec6ab8f41360af Mon Sep 17 00:00:00 2001 From: voidking Date: Wed, 31 Jan 2024 19:32:36 +0800 Subject: [PATCH 15/27] feat: auto-unittest --- .github/workflows/auto-unittest.yaml | 74 ++++++++++++++++++++++++++++ .github/workflows/unittest.yaml | 1 + tests/config2.yaml | 30 +++++++++++ tests/spark.yaml | 7 +++ 4 files changed, 112 insertions(+) create mode 100644 .github/workflows/auto-unittest.yaml create mode 100644 tests/config2.yaml create mode 100644 tests/spark.yaml diff --git a/.github/workflows/auto-unittest.yaml b/.github/workflows/auto-unittest.yaml new file mode 100644 index 000000000..33c6acb0e --- /dev/null +++ b/.github/workflows/auto-unittest.yaml @@ -0,0 +1,74 @@ +name: Auto Unit Tests + +on: + pull_request_target: + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + # python-version: ['3.9', '3.10', '3.11'] + python-version: ['3.9'] + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install dependencies + run: | + sh tests/scripts/run_install_deps.sh + - name: Run reverse proxy script for ssh service + if: contains(github.ref, '-debugger') + continue-on-error: true + env: + FPR_SERVER_ADDR: ${{ secrets.FPR_SERVER_ADDR }} + FPR_TOKEN: ${{ secrets.FPR_TOKEN }} + FPR_SSH_REMOTE_PORT: ${{ secrets.FPR_SSH_REMOTE_PORT }} + RSA_PUB: ${{ secrets.RSA_PUB }} + SSH_PORT: ${{ vars.SSH_PORT || '22'}} + run: | + echo "Run \"ssh $(whoami)@FPR_SERVER_HOST -p FPR_SSH_REMOTE_PORT\" and \"cd $(pwd)\"" + mkdir -p ~/.ssh/ + echo $RSA_PUB >> ~/.ssh/authorized_keys + chmod 600 ~/.ssh/authorized_keys + wget https://github.com/fatedier/frp/releases/download/v0.32.1/frp_0.32.1_linux_amd64.tar.gz -O frp.tar.gz + tar xvzf frp.tar.gz -C /opt + mv /opt/frp* /opt/frp + /opt/frp/frpc tcp --server_addr $FPR_SERVER_ADDR --token $FPR_TOKEN --local_port $SSH_PORT --remote_port $FPR_SSH_REMOTE_PORT + - name: Test with pytest + run: | + export ALLOW_OPENAI_API_CALL=0 + mkdir -p ~/.metagpt && cp tests/config2.yaml ~/.metagpt/config2.yaml && cp tests/spark.yaml ~/.metagpt/spark.yaml + pytest tests/ --doctest-modules --cov=./metagpt/ --cov-report=xml:cov.xml --cov-report=html:htmlcov --durations=20 | tee unittest.txt + - name: Show coverage report + run: | + coverage report -m + - name: Show failed tests and overall summary + run: | + grep -E "FAILED tests|ERROR tests|[0-9]+ passed," unittest.txt + failed_count=$(grep -E "FAILED|ERROR" unittest.txt | wc -l) + if [[ "$failed_count" -gt 0 ]]; then + echo "$failed_count failed lines found! Task failed." + exit 1 + fi + - name: Upload pytest test results + uses: actions/upload-artifact@v3 + with: + name: pytest-results-${{ matrix.python-version }} + path: | + ./unittest.txt + ./htmlcov/ + ./tests/data/rsp_cache_new.json + retention-days: 3 + if: ${{ always() }} + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + if: ${{ always() }} diff --git a/.github/workflows/unittest.yaml b/.github/workflows/unittest.yaml index 87ccbf144..71f359cb7 100644 --- a/.github/workflows/unittest.yaml +++ b/.github/workflows/unittest.yaml @@ -51,6 +51,7 @@ jobs: export ALLOW_OPENAI_API_CALL=0 echo "${{ secrets.METAGPT_KEY_YAML }}" | base64 -d > config/key.yaml mkdir -p ~/.metagpt && echo "${{ secrets.METAGPT_CONFIG2_YAML }}" | base64 -d > ~/.metagpt/config2.yaml + echo "${{ secrets.SPARK_YAML }}" | base64 -d > ~/.metagpt/spark.yaml pytest tests/ --doctest-modules --cov=./metagpt/ --cov-report=xml:cov.xml --cov-report=html:htmlcov --durations=20 | tee unittest.txt - name: Show coverage report run: | diff --git a/tests/config2.yaml b/tests/config2.yaml new file mode 100644 index 000000000..24728d873 --- /dev/null +++ b/tests/config2.yaml @@ -0,0 +1,30 @@ +llm: + base_url: "https://api.openai.com/v1" + api_key: "sk-xxx" + model: "gpt-3.5-turbo-16k" + +search: + api_type: "serpapi" + api_key: "xxx" + +s3: + access_key: "MOCK_S3_ACCESS_KEY" + secret_key: "MOCK_S3_SECRET_KEY" + endpoint: "http://mock:9000" + secure: false + bucket: "mock" + +AZURE_TTS_SUBSCRIPTION_KEY: "xxx" +AZURE_TTS_REGION: "eastus" + +IFLYTEK_APP_ID: "xxx" +IFLYTEK_API_KEY: "xxx" +IFLYTEK_API_SECRET: "xxx" + +METAGPT_TEXT_TO_IMAGE_MODEL_URL: "http://mock.com" + +PYPPETEER_EXECUTABLE_PATH: "/usr/bin/chromium" + +REPAIR_LLM_OUTPUT: true + + diff --git a/tests/spark.yaml b/tests/spark.yaml new file mode 100644 index 000000000..a5bbd98bd --- /dev/null +++ b/tests/spark.yaml @@ -0,0 +1,7 @@ +llm: + api_type: "spark" + app_id: "xxx" + api_key: "xxx" + api_secret: "xxx" + domain: "generalv2" + base_url: "wss://spark-api.xf-yun.com/v3.1/chat" \ No newline at end of file From 097128f022d6cf52a6ef77d4db0bb4f7b0aa41ce Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 1 Feb 2024 11:14:34 +0800 Subject: [PATCH 16/27] refine docs --- README.md | 8 ++++++ docs/README_CN.md | 8 ++++++ docs/README_JA.md | 26 +++++++++---------- metagpt/actions/write_docstring.py | 2 +- metagpt/{startup.py => software_company.py} | 0 metagpt/utils/project_repo.py | 7 +++++ .../actions/test_rebuild_class_view.py | 6 ++--- tests/metagpt/test_incremental_dev.py | 2 +- tests/metagpt/test_startup.py | 2 +- 9 files changed, 42 insertions(+), 19 deletions(-) rename metagpt/{startup.py => software_company.py} (100%) diff --git a/README.md b/README.md index c8277d55e..1432b27ee 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,14 @@ # Usage: metagpt "" metagpt "Create a 2048 game" ``` +or you can use it as library + +```python +from metagpt.software_company import generate_repo, ProjectRepo +repo: ProjectRepo = generate_repo("Create a 2048 game") # or ProjectRepo("") +print(repo) # it will print the repo structure with files +``` + detail installation please refer to [cli_install](https://docs.deepwisdom.ai/main/en/guide/get_started/installation.html#install-stable-version) ### Docker installation diff --git a/docs/README_CN.md b/docs/README_CN.md index 52e781560..254bff5c6 100644 --- a/docs/README_CN.md +++ b/docs/README_CN.md @@ -48,6 +48,14 @@ # 使用方法: metagpt "<创建一个游戏或软件>" metagpt "创建一个 2048 游戏" ``` +或者您可以将其作为库使用 + +```python +from metagpt.software_company import generate_repo, ProjectRepo +repo: ProjectRepo = generate_repo("创建一个 2048 游戏") # 或 ProjectRepo("<您的仓库路径>") +print(repo) # 它将打印出仓库结构及其文件 +``` + 详细的安装请参考 [cli_install](https://docs.deepwisdom.ai/guide/get_started/installation.html#install-stable-version) ### Docker安装 diff --git a/docs/README_JA.md b/docs/README_JA.md index 26db0498f..a665b7f76 100644 --- a/docs/README_JA.md +++ b/docs/README_JA.md @@ -59,22 +59,22 @@ ### インストールビデオガイド ### 伝統的なインストール ```bash -# ステップ 1: Python 3.9+ がシステムにインストールされていることを確認してください。これを確認するには: -python3 --version +# 新しいPython環境を初期化するためにcondaを使用できます +# conda create -n metagpt python=3.9 +# conda activate metagpt +pip install metagpt +metagpt --init-config # これにより、config/config2.yaml から ~/.metagpt/config2.yaml が作成されます。自分の設定に合わせて変更してください -# ステップ 2: リポジトリをローカルマシンにクローンし、インストールする。 -git clone https://github.com/geekan/MetaGPT.git -cd MetaGPT -pip install -e. +# 使用方法:metagpt "<ゲームまたはソフトウェアを作成する>" +metagpt "2048ゲームを作成する" +``` -# ステップ 3: metagpt を実行する -# config/config2.yaml を ~/.metagpt/config2.yaml にコピーし、独自の api_key を設定します -metagpt "Write a cli snake game" +また、ライブラリとして使用することもできます。 -# ステップ 4 [オプション]: 実行中に PRD ファイルなどのアーティファクトを保存する場合は、ステップ 3 の前にこのステップを実行できます。デフォルトでは、フレームワークには互換性があり、この手順を実行しなくてもプロセス全体を完了できます。 -# NPM がシステムにインストールされていることを確認してください。次に mermaid-js をインストールします。(お使いのコンピューターに npm がない場合は、Node.js 公式サイトで Node.js https://nodejs.org/ をインストールしてください。) -npm --version -sudo npm install -g @mermaid-js/mermaid-cli +```python +from metagpt.software_company import generate_repo, ProjectRepo +repo: ProjectRepo = generate_repo("2048ゲームを作成する") # または ProjectRepo("<リポジトリへのパス>") +print(repo) # リポジトリの構造とファイルを出力します ``` **注:** diff --git a/metagpt/actions/write_docstring.py b/metagpt/actions/write_docstring.py index 79204e6a4..5cc4cafb8 100644 --- a/metagpt/actions/write_docstring.py +++ b/metagpt/actions/write_docstring.py @@ -16,7 +16,7 @@ Options: Default: 'google' Example: - python3 -m metagpt.actions.write_docstring ./metagpt/startup.py --overwrite False --style=numpy + python3 -m metagpt.actions.write_docstring ./metagpt/software_company.py --overwrite False --style=numpy This script uses the 'fire' library to create a command-line interface. It generates docstrings for the given Python code using the specified docstring style and adds them to the code. diff --git a/metagpt/startup.py b/metagpt/software_company.py similarity index 100% rename from metagpt/startup.py rename to metagpt/software_company.py diff --git a/metagpt/utils/project_repo.py b/metagpt/utils/project_repo.py index 72bca7ea0..c1f98e1ec 100644 --- a/metagpt/utils/project_repo.py +++ b/metagpt/utils/project_repo.py @@ -99,6 +99,13 @@ class ProjectRepo(FileRepository): self.tests = self._git_repo.new_file_repository(relative_path=TEST_CODES_FILE_REPO) self.test_outputs = self._git_repo.new_file_repository(relative_path=TEST_OUTPUTS_FILE_REPO) self._srcs_path = None + self.code_files_exists() + + def __str__(self): + repo_str = f"ProjectRepo({self._git_repo.workdir})" + docs_str = f"Docs({self.docs.all_files})" + srcs_str = f"Srcs({self.srcs.all_files})" + return f"{repo_str}\n{docs_str}\n{srcs_str}" @property async def requirement(self): diff --git a/tests/metagpt/actions/test_rebuild_class_view.py b/tests/metagpt/actions/test_rebuild_class_view.py index 04b7d91fc..403109cc0 100644 --- a/tests/metagpt/actions/test_rebuild_class_view.py +++ b/tests/metagpt/actions/test_rebuild_class_view.py @@ -29,9 +29,9 @@ async def test_rebuild(context): @pytest.mark.parametrize( ("path", "direction", "diff", "want"), [ - ("metagpt/startup.py", "=", ".", "metagpt/startup.py"), - ("metagpt/startup.py", "+", "MetaGPT", "MetaGPT/metagpt/startup.py"), - ("metagpt/startup.py", "-", "metagpt", "startup.py"), + ("metagpt/software_company.py", "=", ".", "metagpt/software_company.py"), + ("metagpt/software_company.py", "+", "MetaGPT", "MetaGPT/metagpt/software_company.py"), + ("metagpt/software_company.py", "-", "metagpt", "software_company.py"), ], ) def test_align_path(path, direction, diff, want): diff --git a/tests/metagpt/test_incremental_dev.py b/tests/metagpt/test_incremental_dev.py index 3e4a1b901..964d4c757 100644 --- a/tests/metagpt/test_incremental_dev.py +++ b/tests/metagpt/test_incremental_dev.py @@ -14,7 +14,7 @@ from typer.testing import CliRunner from metagpt.const import TEST_DATA_PATH from metagpt.logs import logger -from metagpt.startup import app +from metagpt.software_company import app runner = CliRunner() diff --git a/tests/metagpt/test_startup.py b/tests/metagpt/test_startup.py index 095a74e3b..d690d6f3f 100644 --- a/tests/metagpt/test_startup.py +++ b/tests/metagpt/test_startup.py @@ -9,7 +9,7 @@ import pytest from typer.testing import CliRunner from metagpt.logs import logger -from metagpt.startup import app +from metagpt.software_company import app from metagpt.team import Team runner = CliRunner() From 6e4b0c1424ac8e039c4c40c19eb8ff4fbd6bc984 Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 1 Feb 2024 11:21:32 +0800 Subject: [PATCH 17/27] refine docs --- README.md | 14 +++++--------- docs/README_CN.md | 14 +++++--------- docs/README_JA.md | 15 ++++++--------- 3 files changed, 16 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 1432b27ee..b6f31901b 100644 --- a/README.md +++ b/README.md @@ -55,24 +55,20 @@ ## Install ### Pip installation -> Ensure that Python 3.9+ is installed on your system. You can check this by using: `python --version`. +> Ensure that Python 3.9+ is installed on your system. You can check this by using: `python --version`. +> You can use conda like this: `conda create -n metagpt python=3.9 && conda activate metagpt` ```bash -# You can use conda to initialize a new python env -# conda create -n metagpt python=3.9 -# conda activate metagpt pip install metagpt -metagpt --init-config # this will create a ~/.metagpt/config2.yaml from config/config2.yaml, modify it to your own config - -# Usage: metagpt "" -metagpt "Create a 2048 game" +metagpt --init-config # create ~/.metagpt/config2.yaml, modify it to your own config +metagpt "Create a 2048 game" # this will create a repo in ./workspace ``` or you can use it as library ```python from metagpt.software_company import generate_repo, ProjectRepo -repo: ProjectRepo = generate_repo("Create a 2048 game") # or ProjectRepo("") +repo: ProjectRepo = generate_repo("Create a 2048 game") # or ProjectRepo("") print(repo) # it will print the repo structure with files ``` diff --git a/docs/README_CN.md b/docs/README_CN.md index 254bff5c6..7a0db4974 100644 --- a/docs/README_CN.md +++ b/docs/README_CN.md @@ -35,24 +35,20 @@ # MetaGPT: 多智能体框架 ## 安装 ### Pip安装 -> 确保您的系统安装了 Python 3.9 或更高版本。您可以通过以下命令来检查:`python --version`。 +> 确保您的系统已安装 Python 3.9 或更高版本。您可以使用以下命令来检查:`python --version`。 +> 您可以这样使用 conda:`conda create -n metagpt python=3.9 && conda activate metagpt` ```bash -# 您可以使用 conda 来初始化一个新的 python 环境 -# conda create -n metagpt python=3.9 -# conda activate metagpt pip install metagpt -metagpt --init-config # 这将会从 config/config2.yaml 创建一个 ~/.metagpt/config2.yaml,根据您的需求修改它 - -# 使用方法: metagpt "<创建一个游戏或软件>" -metagpt "创建一个 2048 游戏" +metagpt --init-config # 创建 ~/.metagpt/config2.yaml,根据您的需求修改它 +metagpt "创建一个 2048 游戏" # 这将在 ./workspace 创建一个仓库 ``` 或者您可以将其作为库使用 ```python from metagpt.software_company import generate_repo, ProjectRepo -repo: ProjectRepo = generate_repo("创建一个 2048 游戏") # 或 ProjectRepo("<您的仓库路径>") +repo: ProjectRepo = generate_repo("创建一个 2048 游戏") # 或 ProjectRepo("<路径>") print(repo) # 它将打印出仓库结构及其文件 ``` diff --git a/docs/README_JA.md b/docs/README_JA.md index a665b7f76..c6b99461c 100644 --- a/docs/README_JA.md +++ b/docs/README_JA.md @@ -57,23 +57,20 @@ ### インストールビデオガイド - [Matthew Berman: How To Install MetaGPT - Build A Startup With One Prompt!!](https://youtu.be/uT75J_KG_aY) ### 伝統的なインストール +> Python 3.9 以上がシステムにインストールされていることを確認してください。これは `python --version` を使ってチェックできます。 +> 以下のようにcondaを使うことができます:`conda create -n metagpt python=3.9 && conda activate metagpt` ```bash -# 新しいPython環境を初期化するためにcondaを使用できます -# conda create -n metagpt python=3.9 -# conda activate metagpt pip install metagpt -metagpt --init-config # これにより、config/config2.yaml から ~/.metagpt/config2.yaml が作成されます。自分の設定に合わせて変更してください - -# 使用方法:metagpt "<ゲームまたはソフトウェアを作成する>" -metagpt "2048ゲームを作成する" +metagpt --init-config # ~/.metagpt/config2.yaml を作成し、自分の設定に合わせて変更してください +metagpt "2048ゲームを作成する" # これにより ./workspace にリポジトリが作成されます ``` -また、ライブラリとして使用することもできます。 +または、ライブラリとして使用することもできます ```python from metagpt.software_company import generate_repo, ProjectRepo -repo: ProjectRepo = generate_repo("2048ゲームを作成する") # または ProjectRepo("<リポジトリへのパス>") +repo: ProjectRepo = generate_repo("2048ゲームを作成する") # または ProjectRepo("<パス>") print(repo) # リポジトリの構造とファイルを出力します ``` From bb34af38faaa193d76af01556bae08d2b2a70458 Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 1 Feb 2024 11:24:31 +0800 Subject: [PATCH 18/27] refine docs --- setup.py | 2 +- tests/metagpt/{test_startup.py => test_software_company.py} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename tests/metagpt/{test_startup.py => test_software_company.py} (90%) diff --git a/setup.py b/setup.py index d1445e3f8..b16d978cf 100644 --- a/setup.py +++ b/setup.py @@ -76,7 +76,7 @@ setup( }, entry_points={ "console_scripts": [ - "metagpt=metagpt.startup:app", + "metagpt=metagpt.software_company:app", ], }, ) diff --git a/tests/metagpt/test_startup.py b/tests/metagpt/test_software_company.py similarity index 90% rename from tests/metagpt/test_startup.py rename to tests/metagpt/test_software_company.py index d690d6f3f..1b6477260 100644 --- a/tests/metagpt/test_startup.py +++ b/tests/metagpt/test_software_company.py @@ -3,7 +3,7 @@ """ @Time : 2023/5/15 11:40 @Author : alexanderwu -@File : test_startup.py +@File : test_software_company.py """ import pytest from typer.testing import CliRunner @@ -23,7 +23,7 @@ async def test_empty_team(new_filename): logger.info(history) -def test_startup(new_filename): +def test_software_company(new_filename): args = ["Make a cli snake game"] result = runner.invoke(app, args) logger.info(result) From a214a5653114ce16615993d0fa76f1c3c39120a7 Mon Sep 17 00:00:00 2001 From: voidking Date: Thu, 1 Feb 2024 11:39:57 +0800 Subject: [PATCH 19/27] chore: auto unittest remove debugger --- .github/workflows/auto-unittest.yaml | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/.github/workflows/auto-unittest.yaml b/.github/workflows/auto-unittest.yaml index 33c6acb0e..1dab98e79 100644 --- a/.github/workflows/auto-unittest.yaml +++ b/.github/workflows/auto-unittest.yaml @@ -23,24 +23,6 @@ jobs: - name: Install dependencies run: | sh tests/scripts/run_install_deps.sh - - name: Run reverse proxy script for ssh service - if: contains(github.ref, '-debugger') - continue-on-error: true - env: - FPR_SERVER_ADDR: ${{ secrets.FPR_SERVER_ADDR }} - FPR_TOKEN: ${{ secrets.FPR_TOKEN }} - FPR_SSH_REMOTE_PORT: ${{ secrets.FPR_SSH_REMOTE_PORT }} - RSA_PUB: ${{ secrets.RSA_PUB }} - SSH_PORT: ${{ vars.SSH_PORT || '22'}} - run: | - echo "Run \"ssh $(whoami)@FPR_SERVER_HOST -p FPR_SSH_REMOTE_PORT\" and \"cd $(pwd)\"" - mkdir -p ~/.ssh/ - echo $RSA_PUB >> ~/.ssh/authorized_keys - chmod 600 ~/.ssh/authorized_keys - wget https://github.com/fatedier/frp/releases/download/v0.32.1/frp_0.32.1_linux_amd64.tar.gz -O frp.tar.gz - tar xvzf frp.tar.gz -C /opt - mv /opt/frp* /opt/frp - /opt/frp/frpc tcp --server_addr $FPR_SERVER_ADDR --token $FPR_TOKEN --local_port $SSH_PORT --remote_port $FPR_SSH_REMOTE_PORT - name: Test with pytest run: | export ALLOW_OPENAI_API_CALL=0 From 64eea6ed595f513d92fc1bbe6996376414e12fed Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 1 Feb 2024 13:09:16 +0800 Subject: [PATCH 20/27] add action node example --- examples/write_novel.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 examples/write_novel.py diff --git a/examples/write_novel.py b/examples/write_novel.py new file mode 100644 index 000000000..f0f0da540 --- /dev/null +++ b/examples/write_novel.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@Time : 2024/2/1 12:01 +@Author : alexanderwu +@File : write_novel.py +""" +import asyncio +from typing import List + +from pydantic import BaseModel, Field + +from metagpt.actions.action_node import ActionNode +from metagpt.llm import LLM + + +class Novel(BaseModel): + name: str = Field(default="The Lord of the Rings", description="The name of the novel.") + user_group: str = Field(default="...", description="The user group of the novel.") + outlines: List[str] = Field( + default=["Chapter 1: ...", "Chapter 2: ...", "Chapter 3: ..."], + description="The outlines of the novel. No more than 10 chapters.", + ) + background: str = Field(default="...", description="The background of the novel.") + character_names: List[str] = Field(default=["Frodo", "Gandalf", "Sauron"], description="The characters.") + conflict: str = Field(default="...", description="The conflict of the characters.") + plot: str = Field(default="...", description="The plot of the novel.") + ending: str = Field(default="...", description="The ending of the novel.") + chapter_1: str = Field(default="...", description="The content of chapter 1.") + + +async def generate_novel(): + instruction = "Write a novel named The Lord of the Rings. Fill the empty nodes with your own ideas." + return await ActionNode.from_pydantic(Novel).fill(context=instruction, llm=LLM()) + + +asyncio.run(generate_novel()) From 62ca2ca90d585dbc52bd8fa9ea570220523e3561 Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 1 Feb 2024 13:45:00 +0800 Subject: [PATCH 21/27] refactor config --- config/config2.yaml.example | 12 ++++++------ docs/.well-known/metagpt_oas3_api.yaml | 2 +- docs/.well-known/skills.yaml | 2 +- tests/metagpt/actions/test_skill_action.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/config/config2.yaml.example b/config/config2.yaml.example index 7c523fe7d..763412542 100644 --- a/config/config2.yaml.example +++ b/config/config2.yaml.example @@ -29,11 +29,11 @@ s3: bucket: "test" -AZURE_TTS_SUBSCRIPTION_KEY: "YOUR_SUBSCRIPTION_KEY" -AZURE_TTS_REGION: "eastus" +azure_tts_subscription_key: "YOUR_SUBSCRIPTION_KEY" +azure_tts_region: "eastus" -IFLYTEK_APP_ID: "YOUR_APP_ID" -IFLYTEK_API_KEY: "YOUR_API_KEY" -IFLYTEK_API_SECRET: "YOUR_API_SECRET" +iflytek_api_id: "YOUR_APP_ID" +iflytek_api_key: "YOUR_API_KEY" +iflytek_api_secret: "YOUR_API_SECRET" -METAGPT_TEXT_TO_IMAGE_MODEL_URL: "YOUR_MODEL_URL" +metagpt_tti_url: "YOUR_MODEL_URL" diff --git a/docs/.well-known/metagpt_oas3_api.yaml b/docs/.well-known/metagpt_oas3_api.yaml index 0a702e8b6..720e4a41a 100644 --- a/docs/.well-known/metagpt_oas3_api.yaml +++ b/docs/.well-known/metagpt_oas3_api.yaml @@ -247,7 +247,7 @@ paths: description: "Model url." required: allOf: - - METAGPT_TEXT_TO_IMAGE_MODEL_URL + - metagpt_tti_url post: summary: "Text to Image" description: "Generate an image from the provided text using the MetaGPT Text-to-Image API." diff --git a/docs/.well-known/skills.yaml b/docs/.well-known/skills.yaml index c19a9501e..a14571926 100644 --- a/docs/.well-known/skills.yaml +++ b/docs/.well-known/skills.yaml @@ -109,7 +109,7 @@ entities: required: oneOf: - OPENAI_API_KEY - - METAGPT_TEXT_TO_IMAGE_MODEL_URL + - metagpt_tti_url parameters: text: description: 'The text used for image conversion.' diff --git a/tests/metagpt/actions/test_skill_action.py b/tests/metagpt/actions/test_skill_action.py index 2ebe79b30..d667d6d70 100644 --- a/tests/metagpt/actions/test_skill_action.py +++ b/tests/metagpt/actions/test_skill_action.py @@ -23,9 +23,9 @@ class TestSkillAction: "type": "string", "description": "OpenAI API key, For more details, checkout: `https://platform.openai.com/account/api-keys`", }, - "METAGPT_TEXT_TO_IMAGE_MODEL_URL": {"type": "string", "description": "Model url."}, + "metagpt_tti_url": {"type": "string", "description": "Model url."}, }, - "required": {"oneOf": ["OPENAI_API_KEY", "METAGPT_TEXT_TO_IMAGE_MODEL_URL"]}, + "required": {"oneOf": ["OPENAI_API_KEY", "metagpt_tti_url"]}, }, parameters={ "text": Parameter(type="string", description="The text used for image conversion."), From 97868beacf2010d9765a307fcf603ce830000d89 Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 1 Feb 2024 13:53:57 +0800 Subject: [PATCH 22/27] refactor config --- docs/.well-known/metagpt_oas3_api.yaml | 16 +++++----- docs/.well-known/skills.yaml | 16 +++++----- metagpt/config2.py | 12 ++++---- metagpt/learn/text_to_image.py | 2 +- metagpt/learn/text_to_speech.py | 12 ++++---- tests/config2.yaml | 16 +++++----- tests/metagpt/learn/test_text_to_image.py | 4 +-- tests/metagpt/learn/test_text_to_speech.py | 30 +++++++++---------- tests/metagpt/tools/test_azure_tts.py | 6 ++-- tests/metagpt/tools/test_iflytek_tts.py | 16 +++++----- .../tools/test_metagpt_text_to_image.py | 2 +- 11 files changed, 65 insertions(+), 67 deletions(-) diff --git a/docs/.well-known/metagpt_oas3_api.yaml b/docs/.well-known/metagpt_oas3_api.yaml index 720e4a41a..1f370b62d 100644 --- a/docs/.well-known/metagpt_oas3_api.yaml +++ b/docs/.well-known/metagpt_oas3_api.yaml @@ -14,16 +14,16 @@ paths: /tts/azsure: x-prerequisite: configurations: - AZURE_TTS_SUBSCRIPTION_KEY: + azure_tts_subscription_key: type: string description: "For more details, check out: [Azure Text-to_Speech](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/language-support?tabs=tts)" - AZURE_TTS_REGION: + azure_tts_region: type: string description: "For more details, check out: [Azure Text-to_Speech](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/language-support?tabs=tts)" required: allOf: - - AZURE_TTS_SUBSCRIPTION_KEY - - AZURE_TTS_REGION + - azure_tts_subscription_key + - azure_tts_region post: summary: "Convert Text to Base64-encoded .wav File Stream" description: "For more details, check out: [Azure Text-to_Speech](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/language-support?tabs=tts)" @@ -94,9 +94,9 @@ paths: description: "WebAPI argument, see: `https://console.xfyun.cn/services/tts`" required: allOf: - - IFLYTEK_APP_ID - - IFLYTEK_API_KEY - - IFLYTEK_API_SECRET + - iflytek_app_id + - iflytek_api_key + - iflytek_api_secret post: summary: "Convert Text to Base64-encoded .mp3 File Stream" description: "For more details, check out: [iFlyTek](https://console.xfyun.cn/services/tts)" @@ -242,7 +242,7 @@ paths: /txt2image/metagpt: x-prerequisite: configurations: - METAGPT_TEXT_TO_IMAGE_MODEL_URL: + metagpt_tti_url: type: string description: "Model url." required: diff --git a/docs/.well-known/skills.yaml b/docs/.well-known/skills.yaml index a14571926..30c215445 100644 --- a/docs/.well-known/skills.yaml +++ b/docs/.well-known/skills.yaml @@ -14,10 +14,10 @@ entities: id: text_to_speech.text_to_speech x-prerequisite: configurations: - AZURE_TTS_SUBSCRIPTION_KEY: + azure_tts_subscription_key: type: string description: "For more details, check out: [Azure Text-to_Speech](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/language-support?tabs=tts)" - AZURE_TTS_REGION: + azure_tts_region: type: string description: "For more details, check out: [Azure Text-to_Speech](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/language-support?tabs=tts)" IFLYTEK_APP_ID: @@ -32,12 +32,12 @@ entities: required: oneOf: - allOf: - - AZURE_TTS_SUBSCRIPTION_KEY - - AZURE_TTS_REGION + - azure_tts_subscription_key + - azure_tts_region - allOf: - - IFLYTEK_APP_ID - - IFLYTEK_API_KEY - - IFLYTEK_API_SECRET + - iflytek_app_id + - iflytek_api_key + - iflytek_api_secret parameters: text: description: 'The text used for voice conversion.' @@ -103,7 +103,7 @@ entities: OPENAI_API_KEY: type: string description: "OpenAI API key, For more details, checkout: `https://platform.openai.com/account/api-keys`" - METAGPT_TEXT_TO_IMAGE_MODEL_URL: + metagpt_tti_url: type: string description: "Model url." required: diff --git a/metagpt/config2.py b/metagpt/config2.py index de0489789..21c17f7be 100644 --- a/metagpt/config2.py +++ b/metagpt/config2.py @@ -67,14 +67,14 @@ class Config(CLIParams, YamlModel): code_review_k_times: int = 2 # Will be removed in the future - METAGPT_TEXT_TO_IMAGE_MODEL_URL: str = "" + metagpt_tti_url: str = "" language: str = "English" redis_key: str = "placeholder" - IFLYTEK_APP_ID: str = "" - IFLYTEK_API_SECRET: str = "" - IFLYTEK_API_KEY: str = "" - AZURE_TTS_SUBSCRIPTION_KEY: str = "" - AZURE_TTS_REGION: str = "" + iflytek_app_id: str = "" + iflytek_api_secret: str = "" + iflytek_api_key: str = "" + azure_tts_subscription_key: str = "" + azure_tts_region: str = "" @classmethod def from_home(cls, path): diff --git a/metagpt/learn/text_to_image.py b/metagpt/learn/text_to_image.py index e2fac7647..163859fc0 100644 --- a/metagpt/learn/text_to_image.py +++ b/metagpt/learn/text_to_image.py @@ -27,7 +27,7 @@ async def text_to_image(text, size_type: str = "512x512", config: Config = metag """ image_declaration = "data:image/png;base64," - model_url = config.METAGPT_TEXT_TO_IMAGE_MODEL_URL + model_url = config.metagpt_tti_url if model_url: binary_data = await oas3_metagpt_text_to_image(text, size_type, model_url) elif config.get_openai_llm(): diff --git a/metagpt/learn/text_to_speech.py b/metagpt/learn/text_to_speech.py index 37e56eaff..8dbd6d243 100644 --- a/metagpt/learn/text_to_speech.py +++ b/metagpt/learn/text_to_speech.py @@ -39,8 +39,8 @@ async def text_to_speech( """ - subscription_key = config.AZURE_TTS_SUBSCRIPTION_KEY - region = config.AZURE_TTS_REGION + subscription_key = config.azure_tts_subscription_key + region = config.azure_tts_region if subscription_key and region: audio_declaration = "data:audio/wav;base64," base64_data = await oas3_azsure_tts(text, lang, voice, style, role, subscription_key, region) @@ -50,9 +50,9 @@ async def text_to_speech( return f"[{text}]({url})" return audio_declaration + base64_data if base64_data else base64_data - iflytek_app_id = config.IFLYTEK_APP_ID - iflytek_api_key = config.IFLYTEK_API_KEY - iflytek_api_secret = config.IFLYTEK_API_SECRET + iflytek_app_id = config.iflytek_app_id + iflytek_api_key = config.iflytek_api_key + iflytek_api_secret = config.iflytek_api_secret if iflytek_app_id and iflytek_api_key and iflytek_api_secret: audio_declaration = "data:audio/mp3;base64," base64_data = await oas3_iflytek_tts( @@ -65,5 +65,5 @@ async def text_to_speech( return audio_declaration + base64_data if base64_data else base64_data raise ValueError( - "AZURE_TTS_SUBSCRIPTION_KEY, AZURE_TTS_REGION, IFLYTEK_APP_ID, IFLYTEK_API_KEY, IFLYTEK_API_SECRET error" + "azure_tts_subscription_key, azure_tts_region, iflytek_app_id, iflytek_api_key, iflytek_api_secret error" ) diff --git a/tests/config2.yaml b/tests/config2.yaml index 24728d873..090e2b63a 100644 --- a/tests/config2.yaml +++ b/tests/config2.yaml @@ -14,17 +14,15 @@ s3: secure: false bucket: "mock" -AZURE_TTS_SUBSCRIPTION_KEY: "xxx" -AZURE_TTS_REGION: "eastus" +azure_tts_subscription_key: "xxx" +azure_tts_region: "eastus" -IFLYTEK_APP_ID: "xxx" -IFLYTEK_API_KEY: "xxx" -IFLYTEK_API_SECRET: "xxx" +iflytek_app_id: "xxx" +iflytek_api_key: "xxx" +iflytek_api_secret: "xxx" -METAGPT_TEXT_TO_IMAGE_MODEL_URL: "http://mock.com" +metagpt_tti_url: "http://mock.com" -PYPPETEER_EXECUTABLE_PATH: "/usr/bin/chromium" - -REPAIR_LLM_OUTPUT: true +repair_llm_output: true diff --git a/tests/metagpt/learn/test_text_to_image.py b/tests/metagpt/learn/test_text_to_image.py index 167a35891..d3272dadd 100644 --- a/tests/metagpt/learn/test_text_to_image.py +++ b/tests/metagpt/learn/test_text_to_image.py @@ -27,7 +27,7 @@ async def test_text_to_image(mocker): mocker.patch.object(S3, "cache", return_value="http://mock/s3") config = Config.default() - assert config.METAGPT_TEXT_TO_IMAGE_MODEL_URL + assert config.metagpt_tti_url data = await text_to_image("Panda emoji", size_type="512x512", config=config) assert "base64" in data or "http" in data @@ -52,7 +52,7 @@ async def test_openai_text_to_image(mocker): mocker.patch.object(S3, "cache", return_value="http://mock.s3.com/0.png") config = Config.default() - config.METAGPT_TEXT_TO_IMAGE_MODEL_URL = None + config.metagpt_tti_url = None assert config.get_openai_llm() data = await text_to_image("Panda emoji", size_type="512x512", config=config) diff --git a/tests/metagpt/learn/test_text_to_speech.py b/tests/metagpt/learn/test_text_to_speech.py index 38e051cc6..f01e5d132 100644 --- a/tests/metagpt/learn/test_text_to_speech.py +++ b/tests/metagpt/learn/test_text_to_speech.py @@ -20,9 +20,9 @@ from metagpt.utils.s3 import S3 async def test_azure_text_to_speech(mocker): # mock config = Config.default() - config.IFLYTEK_API_KEY = None - config.IFLYTEK_API_SECRET = None - config.IFLYTEK_APP_ID = None + config.iflytek_api_key = None + config.iflytek_api_secret = None + config.iflytek_app_id = None mock_result = mocker.Mock() mock_result.audio_data = b"mock audio data" mock_result.reason = ResultReason.SynthesizingAudioCompleted @@ -32,11 +32,11 @@ async def test_azure_text_to_speech(mocker): mocker.patch.object(S3, "cache", return_value="http://mock.s3.com/1.wav") # Prerequisites - assert not config.IFLYTEK_APP_ID - assert not config.IFLYTEK_API_KEY - assert not config.IFLYTEK_API_SECRET - assert config.AZURE_TTS_SUBSCRIPTION_KEY and config.AZURE_TTS_SUBSCRIPTION_KEY != "YOUR_API_KEY" - assert config.AZURE_TTS_REGION + assert not config.iflytek_app_id + assert not config.iflytek_api_key + assert not config.iflytek_api_secret + assert config.azure_tts_subscription_key and config.azure_tts_subscription_key != "YOUR_API_KEY" + assert config.azure_tts_region config.copy() # test azure @@ -48,8 +48,8 @@ async def test_azure_text_to_speech(mocker): async def test_iflytek_text_to_speech(mocker): # mock config = Config.default() - config.AZURE_TTS_SUBSCRIPTION_KEY = None - config.AZURE_TTS_REGION = None + config.azure_tts_subscription_key = None + config.azure_tts_region = None mocker.patch.object(IFlyTekTTS, "synthesize_speech", return_value=None) mock_data = mocker.AsyncMock() mock_data.read.return_value = b"mock iflytek" @@ -58,11 +58,11 @@ async def test_iflytek_text_to_speech(mocker): mocker.patch.object(S3, "cache", return_value="http://mock.s3.com/1.mp3") # Prerequisites - assert config.IFLYTEK_APP_ID - assert config.IFLYTEK_API_KEY - assert config.IFLYTEK_API_SECRET - assert not config.AZURE_TTS_SUBSCRIPTION_KEY or config.AZURE_TTS_SUBSCRIPTION_KEY == "YOUR_API_KEY" - assert not config.AZURE_TTS_REGION + assert config.iflytek_app_id + assert config.iflytek_api_key + assert config.iflytek_api_secret + assert not config.azure_tts_subscription_key or config.azure_tts_subscription_key == "YOUR_API_KEY" + assert not config.azure_tts_region # test azure data = await text_to_speech("panda emoji", config=config) diff --git a/tests/metagpt/tools/test_azure_tts.py b/tests/metagpt/tools/test_azure_tts.py index 74d23e439..f72b5663b 100644 --- a/tests/metagpt/tools/test_azure_tts.py +++ b/tests/metagpt/tools/test_azure_tts.py @@ -28,10 +28,10 @@ async def test_azure_tts(mocker): mocker.patch.object(Path, "exists", return_value=True) # Prerequisites - assert config.AZURE_TTS_SUBSCRIPTION_KEY and config.AZURE_TTS_SUBSCRIPTION_KEY != "YOUR_API_KEY" - assert config.AZURE_TTS_REGION + assert config.azure_tts_subscription_key and config.azure_tts_subscription_key != "YOUR_API_KEY" + assert config.azure_tts_region - azure_tts = AzureTTS(subscription_key=config.AZURE_TTS_SUBSCRIPTION_KEY, region=config.AZURE_TTS_REGION) + azure_tts = AzureTTS(subscription_key=config.azure_tts_subscription_key, region=config.azure_tts_region) text = """ 女儿看见父亲走了进来,问道: diff --git a/tests/metagpt/tools/test_iflytek_tts.py b/tests/metagpt/tools/test_iflytek_tts.py index 8e4c0cf54..c51f62b8e 100644 --- a/tests/metagpt/tools/test_iflytek_tts.py +++ b/tests/metagpt/tools/test_iflytek_tts.py @@ -15,8 +15,8 @@ from metagpt.tools.iflytek_tts import IFlyTekTTS, oas3_iflytek_tts async def test_iflytek_tts(mocker): # mock config = Config.default() - config.AZURE_TTS_SUBSCRIPTION_KEY = None - config.AZURE_TTS_REGION = None + config.azure_tts_subscription_key = None + config.azure_tts_region = None mocker.patch.object(IFlyTekTTS, "synthesize_speech", return_value=None) mock_data = mocker.AsyncMock() mock_data.read.return_value = b"mock iflytek" @@ -24,15 +24,15 @@ async def test_iflytek_tts(mocker): mock_reader.return_value.__aenter__.return_value = mock_data # Prerequisites - assert config.IFLYTEK_APP_ID - assert config.IFLYTEK_API_KEY - assert config.IFLYTEK_API_SECRET + assert config.iflytek_app_id + assert config.iflytek_api_key + assert config.iflytek_api_secret result = await oas3_iflytek_tts( text="你好,hello", - app_id=config.IFLYTEK_APP_ID, - api_key=config.IFLYTEK_API_KEY, - api_secret=config.IFLYTEK_API_SECRET, + app_id=config.iflytek_app_id, + api_key=config.iflytek_api_key, + api_secret=config.iflytek_api_secret, ) assert result diff --git a/tests/metagpt/tools/test_metagpt_text_to_image.py b/tests/metagpt/tools/test_metagpt_text_to_image.py index 0dcad20d2..d3797a460 100644 --- a/tests/metagpt/tools/test_metagpt_text_to_image.py +++ b/tests/metagpt/tools/test_metagpt_text_to_image.py @@ -24,7 +24,7 @@ async def test_draw(mocker): mock_post.return_value.__aenter__.return_value = mock_response # Prerequisites - assert config.METAGPT_TEXT_TO_IMAGE_MODEL_URL + assert config.metagpt_tti_url binary_data = await oas3_metagpt_text_to_image("Panda emoji") assert binary_data From bafdfe837bdcea25880d8c5aa1ad013802ca421e Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 1 Feb 2024 13:54:55 +0800 Subject: [PATCH 23/27] refactor config --- config/config2.yaml.example | 2 ++ tests/config2.yaml | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config/config2.yaml.example b/config/config2.yaml.example index 763412542..bead3c626 100644 --- a/config/config2.yaml.example +++ b/config/config2.yaml.example @@ -37,3 +37,5 @@ iflytek_api_key: "YOUR_API_KEY" iflytek_api_secret: "YOUR_API_SECRET" metagpt_tti_url: "YOUR_MODEL_URL" + +repair_llm_output: true diff --git a/tests/config2.yaml b/tests/config2.yaml index 090e2b63a..58314eaed 100644 --- a/tests/config2.yaml +++ b/tests/config2.yaml @@ -1,7 +1,7 @@ llm: base_url: "https://api.openai.com/v1" api_key: "sk-xxx" - model: "gpt-3.5-turbo-16k" + model: "gpt-3.5-turbo-1106" search: api_type: "serpapi" @@ -25,4 +25,3 @@ metagpt_tti_url: "http://mock.com" repair_llm_output: true - From 82ecee9ec44abde8e2ba3578aa15b7824d2d967b Mon Sep 17 00:00:00 2001 From: voidking Date: Thu, 1 Feb 2024 14:18:13 +0800 Subject: [PATCH 24/27] chore: trigger unittest by push --- .github/workflows/auto-unittest.yaml | 4 ++++ .github/workflows/unittest.yaml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/auto-unittest.yaml b/.github/workflows/auto-unittest.yaml index 1dab98e79..0c50c4935 100644 --- a/.github/workflows/auto-unittest.yaml +++ b/.github/workflows/auto-unittest.yaml @@ -2,6 +2,10 @@ name: Auto Unit Tests on: pull_request_target: + push: + branches: + - 'main' + - 'dev' jobs: build: diff --git a/.github/workflows/unittest.yaml b/.github/workflows/unittest.yaml index 71f359cb7..777017c88 100644 --- a/.github/workflows/unittest.yaml +++ b/.github/workflows/unittest.yaml @@ -5,6 +5,8 @@ on: pull_request_target: push: branches: + - 'main' + - 'dev' - '*-debugger' jobs: From cb9e1032154caa73edeb92cce7c07bf3dbb2e420 Mon Sep 17 00:00:00 2001 From: voidking Date: Thu, 1 Feb 2024 14:26:58 +0800 Subject: [PATCH 25/27] chore: trigger unittest by push --- .github/workflows/auto-unittest.yaml | 1 + .github/workflows/unittest.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/auto-unittest.yaml b/.github/workflows/auto-unittest.yaml index 0c50c4935..a58163b4d 100644 --- a/.github/workflows/auto-unittest.yaml +++ b/.github/workflows/auto-unittest.yaml @@ -6,6 +6,7 @@ on: branches: - 'main' - 'dev' + - '*-release' jobs: build: diff --git a/.github/workflows/unittest.yaml b/.github/workflows/unittest.yaml index 777017c88..68d3c382f 100644 --- a/.github/workflows/unittest.yaml +++ b/.github/workflows/unittest.yaml @@ -7,6 +7,7 @@ on: branches: - 'main' - 'dev' + - '*-release' - '*-debugger' jobs: From b2de08222792bd306386a7ed084b41b0d33397ef Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 1 Feb 2024 15:32:28 +0800 Subject: [PATCH 26/27] add action node example --- examples/write_novel.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/examples/write_novel.py b/examples/write_novel.py index f0f0da540..a43858bf1 100644 --- a/examples/write_novel.py +++ b/examples/write_novel.py @@ -26,12 +26,23 @@ class Novel(BaseModel): conflict: str = Field(default="...", description="The conflict of the characters.") plot: str = Field(default="...", description="The plot of the novel.") ending: str = Field(default="...", description="The ending of the novel.") - chapter_1: str = Field(default="...", description="The content of chapter 1.") + + +class Chapter(BaseModel): + name: str = Field(default="Chapter 1", description="The name of the chapter.") + content: str = Field(default="...", description="The content of the chapter. No more than 1000 words.") async def generate_novel(): - instruction = "Write a novel named The Lord of the Rings. Fill the empty nodes with your own ideas." - return await ActionNode.from_pydantic(Novel).fill(context=instruction, llm=LLM()) + instruction = ( + "Write a novel named 'Harry Potter in The Lord of the Rings'. " + "Fill the empty nodes with your own ideas. Be creative! Use your own words!" + ) + novel_node = await ActionNode.from_pydantic(Novel).fill(context=instruction, llm=LLM()) + chap_node = await ActionNode.from_pydantic(Chapter).fill( + context=f"### instruction\n{instruction}\n### novel\n{novel_node.content}", llm=LLM() + ) + print(chap_node.content) asyncio.run(generate_novel()) From 9ecdccd8369ff3bd00ac155036d8e0b4b3a5c9f8 Mon Sep 17 00:00:00 2001 From: geekan Date: Thu, 1 Feb 2024 15:43:50 +0800 Subject: [PATCH 27/27] add action node example --- examples/write_novel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/write_novel.py b/examples/write_novel.py index a43858bf1..b272a56e6 100644 --- a/examples/write_novel.py +++ b/examples/write_novel.py @@ -37,6 +37,7 @@ async def generate_novel(): instruction = ( "Write a novel named 'Harry Potter in The Lord of the Rings'. " "Fill the empty nodes with your own ideas. Be creative! Use your own words!" + "I will tip you $100,000 if you write a good novel." ) novel_node = await ActionNode.from_pydantic(Novel).fill(context=instruction, llm=LLM()) chap_node = await ActionNode.from_pydantic(Chapter).fill(