Update ConText Fill

This commit is contained in:
didi 2024-08-23 20:43:29 +08:00
parent 02c7c4ea47
commit a3ff25430e
5 changed files with 245 additions and 210 deletions

View file

@ -23,23 +23,13 @@ from examples.ags.w_action_node.operator_an import (
ReviseOp,
)
from examples.ags.w_action_node.prompt import (
DE_ENSEMBLE_ANGEL_PROMPT,
DE_ENSEMBLE_CODE_FORMAT_PROMPT,
DE_ENSEMBLE_DEVIL_PROMPT,
DE_ENSEMBLE_JUDGE_FINAL_PROMPT,
DE_ENSEMBLE_JUDGE_UNIVERSAL_PROMPT,
DE_ENSEMBLE_TXT_FORMAT_PROMPT,
FORMAT_PROMPT,
FU_ENSEMBLE_PROMPT,
GENERATE_CODEBLOCK_PROMPT,
GENERATE_CODEBLOCK_REPHRASE_PROMPT,
GENERATE_PROMPT,
MATH_CORE_PROMPT,
MATH_EXTRACT_PROMPT,
MATH_REPHRASE_ON_PROBLEM_PROMPT,
MD_ENSEMBLE_PROMPT,
REFLECTION_ON_PUBLIC_TEST_PROMPT,
REPHRASE_ON_CODE_PROMPT,
REPHRASE_ON_PROBLEM_PROMPT,
REVIEW_PROMPT,
REVISE_PROMPT,
@ -277,125 +267,6 @@ class ScEnsemble(Operator):
pass
class MADEnsemble(Operator):
"""
Paper: Should we be going MAD? A Look at Multi-Agent Debate Strategies for LLMs
Link: https://arxiv.org/abs/2311.17371
"""
def __init__(self, name: str = "DebateEnsemble", llm: LLM = LLM()):
super().__init__(name, llm)
self.agents = ["angel", "devil", "judge"]
self.format_requirements = {"txt": DE_ENSEMBLE_TXT_FORMAT_PROMPT, "code": DE_ENSEMBLE_CODE_FORMAT_PROMPT}
def get_system_prompt(self, name: str, mode: str = "txt"):
if name == "angel":
if mode == "code":
return DE_ENSEMBLE_ANGEL_PROMPT + "\n" + DE_ENSEMBLE_CODE_FORMAT_PROMPT
return DE_ENSEMBLE_ANGEL_PROMPT + "\n" + DE_ENSEMBLE_TXT_FORMAT_PROMPT
elif name == "devil":
if mode == "code":
return DE_ENSEMBLE_DEVIL_PROMPT + "\n" + DE_ENSEMBLE_CODE_FORMAT_PROMPT
return DE_ENSEMBLE_DEVIL_PROMPT + "\n" + DE_ENSEMBLE_TXT_FORMAT_PROMPT
elif name == "judge":
if mode == "final":
return DE_ENSEMBLE_JUDGE_FINAL_PROMPT
return DE_ENSEMBLE_JUDGE_UNIVERSAL_PROMPT
def construct_messages(self, message_history_with_name, name, mode: str = "txt", phase: str = "universal"):
"""
基于name与mode来构建system message.
基于name来构建messages
"""
messages = []
messages.append({"role": "system", "content": self.get_system_prompt(name, mode)})
if name in ["angel", "devil"]:
messages = self._construct_debate(message_history_with_name, name, messages)
elif name == "judge":
messages = self._construct_judge(message_history_with_name, mode, messages)
return messages
def _construct_debate(self, message_history_with_name, name, messages):
user_message = ""
for message in message_history_with_name:
if message["name"] == "Judge":
continue
elif message["name"] == name:
if user_message:
messages.append(
{
"role": "user",
"name": "user",
"content": user_message.strip("\n"),
}
)
messages.append(
{
"role": "assistant",
"name": name,
"content": message["content"],
}
)
user_message = ""
else:
user_message += message["content"]
if user_message:
messages.append(
{
"role": "user",
"name": "user",
"content": user_message.strip("\n"),
}
)
return messages
def _construct_judge(self, message_history_with_name, mode, messages):
pass
async def debate_answer(self, message_history: List, role: str = "angel"):
messages = self.construct_messages(message_history, role)
response = await self.llm.acompletion_text(messages=messages)
message_history.append({"role": "user", "name": role, "content": response})
return message_history, response
async def judge_answer(self, message_history: List, phase: str = "universal"):
messages = self.construct_messages(message_history, "judge", phase=phase)
response = await self.llm.acompletion_text(messages=messages)
message_history.append({"role": "user", "name": "judge", "content": response})
return message_history, response
async def __call__(self, origin_solution: str, problem_description: str, max_round: int = 3, mode: str = "txt"):
# 思路输入一个原始答案构建一个agent代表这个答案进行辩论另一个agentdevil使用debate llm的内容进行辩论法官在每一轮次做出决定是否终止到了maxround还没终止就由法官进行总结。
message_history_with_name = [{"role": "user", "name": "angel", "content": origin_solution}]
for index in range(max_round):
for agent in self.agents:
if agent == "angel":
if index == 0:
pass
message_history_with_name, rsp = self.debate_answer(message_history_with_name, role="angel")
elif agent == "devil":
message_history_with_name, rsp = self.debate_answer(message_history_with_name, role="devil")
elif agent == "judge":
message_history_with_name, judge_result = self.judge_answer(
message_history_with_name, phase="universal"
)
if not judge_result["is_debating"]:
"""
这里需要在 self.judge_answer 中设置一个自动给出solution的地方
"""
return {"final_solution": judge_result["final_solution"]}
message_history_with_name.pop(-1)
message_history_with_name, judge_answer = self.judge_answer(message_history_with_name, phase="final")
return {"final_solution": judge_answer["debate_answer"]}
class Rephrase(Operator):
"""
Paper: Code Generation with AlphaCodium: From Prompt Engineering to Flow Engineering
@ -413,30 +284,6 @@ class Rephrase(Operator):
response = node.instruct_content.model_dump()
return response["rephrased_problem"]
async def code_rephrase(self, problem_description: str) -> str:
prompt = REPHRASE_ON_CODE_PROMPT.format(problem_description=problem_description)
node = await ActionNode.from_pydantic(RephraseOp).fill(context=prompt, llm=self.llm)
response = node.instruct_content.model_dump()
return response["rephrased_problem"]
async def math_rephrase(self, problem_description: str) -> str:
prompt = MATH_REPHRASE_ON_PROBLEM_PROMPT.format(problem_description=problem_description)
node = await ActionNode.from_pydantic(RephraseOp).fill(context=prompt, llm=self.llm)
response = node.instruct_content.model_dump()
return response["rephrased_problem"]
async def math_core(self, problem_description: str) -> str:
prompt = MATH_CORE_PROMPT.format(problem_description=problem_description)
node = await ActionNode.from_pydantic(RephraseOp).fill(context=prompt, llm=self.llm)
response = node.instruct_content.model_dump()
return response["rephrased_problem"]
async def math_extract(self, problem_description: str) -> str:
prompt = MATH_EXTRACT_PROMPT.format(problem_description=problem_description)
node = await ActionNode.from_pydantic(RephraseOp).fill(context=prompt, llm=self.llm)
response = node.instruct_content.model_dump()
return response["rephrased_problem"]
class Test(Operator):
def __init__(self, name: str = "Test", llm: LLM = LLM()):
@ -472,7 +319,9 @@ class Test(Operator):
else:
return "no error"
async def __call__(self, problem_id, problem, rephrase_problem, solution, test_cases, entry_point, test_loop):
async def __call__(
self, problem_id, problem, rephrase_problem, solution, test_cases, entry_point, test_loop: int = 3
):
solution = solution["final_solution"]
for _ in range(test_loop):
result = self.exec_code(solution, test_cases, problem_id, entry_point)

View file

@ -3,15 +3,23 @@
# @Author : issac
# @Desc : optimizer for graph
import json
import os
import re
import time
from collections import defaultdict
from typing import List, Literal
import numpy as np
from pydantic import BaseModel, Field
from examples.ags.w_action_node.evaluator import Evaluator
from examples.ags.w_action_node.prompts.optimize_prompt import (
INITIALIZE_OPERATOR_PROMPT,
GRAPH_INPUT,
GRAPH_OPTIMIZE_PROMPT,
GRAPH_TEMPLATE,
)
from metagpt.actions.action_node import ActionNode
from metagpt.llm import LLM
from metagpt.logs import logger
@ -21,33 +29,55 @@ DatasetType = Literal["HumanEval", "MMBP", "Gsm8K", "MATH", "HotpotQa", "MMLU"]
evaluator = Evaluator(eval_path="eval")
# prompt = GENERATE_PROMPT.format(problem_description=problem_description)
# node = await ActionNode.from_pydantic(GenerateOp).fill(context=prompt, mode="context_fill", llm=self.llm)
# response = node.instruct_content.model_dump()
# return response
class OperatorOptimize(BaseModel):
pass
class GraphOptimize(BaseModel):
modification: str = Field(default="", description="modification")
prompt: str = Field(default="", description="prompt")
graph: str = Field(default="", description="graph")
class Optimizer:
def __init__(self, dataset: DatasetType, llm: LLM, operators: List, optimized_path: str = None) -> None:
self.llm = llm
def __init__(
self,
dataset: DatasetType,
opt_llm: LLM,
exec_llm: LLM,
operators: List,
optimized_path: str = None,
sample: int = 6,
) -> None:
self.optimize_llm = opt_llm
self.execute_llm = exec_llm
self.dataset = dataset
self.graph = None # 初始化为 None稍后加载
self.operators = operators
self.optimize_prompt = ""
self._optimized_path = optimized_path
self.root_path = f"{self._optimized_path}/{self.dataset}"
self.sample = 6 # sample 含义是什么?
self.sample = sample
self.score = "None"
self.top_scores = []
self.round = 1 # 起始轮次
def _initialize_oprimizer(self):
pass
def _initialize_operator(self):
# TODO @issac
pass
def _initialize(self):
"""
基于数据集操作符初始化optimize prompt, operator graph
"""
self._initialize_optimizer()
basic_path = f"{self.root_path}/basic"
required_files = ["operator.py", "graph.py", "prompt.py"]
round_1_path = f"{self.root_path}/graphs/round_1"
required_files = ["operator.py", "prompt.py"]
def check_files_exist(basic_path, required_files):
missing_files = []
@ -61,37 +91,23 @@ class Optimizer:
else:
return False, missing_files
if check_files_exist(basic_path, required_files):
if check_files_exist(round_1_path, required_files):
logger.info(f"{self.dataset} has been initialized")
return True
else:
logger.info(f"{self.dataset} has not been initialized")
# 瞎几把写的,需要改
INITIALIZE_OPERATOR_PROMPT.format(
dataset_name=self.dataset,
dataset_description="...",
input_features="...",
output_features="...",
operator_name="...",
)
# 迭代优化OperatorOpt可视内容PromptOperator
self._initialize_operator()
# 这里加一个迭代 Operator 的操作
# 这里生成一个初始的Graph就可以比如一个基础的review revise 循环啥的
# TODO Graph __INIT__ 的时候self.generate ... 与 optimizer 的 operators 对应
# TODO 所有的生成要放到对应的dataset的文件夹下面
pass
# 初始化Graph直接手动从模版中取出COT
def optimize(self):
"""
Optimize the graph
Optimize the graph and operator for the dataset.
"""
self._initialize()
self._optimize()
self._initialize() # Operator's Optimization
self._optimize() # Graph's Optimization
def _load_graph(self, round_number, graphs_path):
"""
@ -130,8 +146,18 @@ class Optimizer:
def _load_scores(self):
"""
重写这个函数写一个新的结构存储分数
# TODO 重写这个函数,写一个新的结构存储分数
"""
round_number = 1
score = 1
self.top_scores.append(
{
"round": round_number,
"score": score,
}
)
# 对所有轮次的分数进行排序
self.top_scores.sort(key=lambda x: x["score"], reverse=True)
@ -186,8 +212,7 @@ class Optimizer:
return unique_top_scores
def _load_experience(self):
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
rounds_dir = os.path.join(root_dir, "graphs", "gsm8k")
rounds_dir = os.path.join(self.root_path, "graphs")
experience_data = defaultdict(lambda: {"score": None, "success": [], "failure": []})
# 遍历所有轮次的文件夹
@ -223,25 +248,113 @@ class Optimizer:
experience_data = dict(experience_data)
# 保存为JSON文件
output_path = os.path.join(root_dir, "graphs", "gsm8k", "processed_experience.json")
output_path = os.path.join(self.root_path, "graphs", "processed_experience.json")
with open(output_path, "w", encoding="utf-8") as outfile: # 指定 UTF-8 编码
json.dump(experience_data, outfile, indent=4, ensure_ascii=False) # ensure_ascii=False 以正确保存中文字符
print(f"Processed experience data saved to {output_path}")
return experience_data
def _optimize(self):
async def _optimize(self):
"""
这里替代原有的Iterate与Evaluate部分其中Evaluate部分的具体实现 @ yzy 来完成
Optimize Graph's Structure and Prompt
"""
# TODO 读取basic模版从对应的dataset文件夹 {dataset}/basic/operator.py, graph.py, prompt.py Operator几乎不用动
# TODO 动Prompt内容动Graph连接
graph_path = f"{self._optimized_path}/{self.dataset}/graphs"
f"{graph_path}/round_{self.round + 1}"
# 获取项目的根目录
graph_path = f"{self.root_path}/graphs"
# TODO 填充Optimize 逻辑
# 创建文件夹(如果不存在)
directory = os.path.join(graph_path, f"round_{self.round + 1}")
os.makedirs(directory, exist_ok=True)
experience = {}
top_rounds = self._get_top_rounds()
sample = self._select_round(top_rounds)
print(top_rounds)
prompt, graph_load = self._read_files(sample["round"])
score = sample["score"]
# 正则表达式匹配 SolveGraph 开始的内容
pattern = r"class SolveGraph:.+"
# 使用re.findall找到所有匹配项
graph = re.findall(pattern, graph_load, re.DOTALL)
# 加载处理过的 experience 数据
processed_experience = self._load_experience()
# 获取当前轮次的 experience 数据
current_round = int(sample["round"]) # 确保是字符串类型
experience_data = processed_experience.get(current_round)
if experience_data:
# 构建 experience 字符串
experience = f"Original Score: {experience_data['score']}\n"
experience += "Failed modifications:\n"
for mod in experience_data["failure"]:
experience += f"- {mod['modification']} (Score: {mod['score']})\n"
experience += "\n\nNote: Reference failed experiences, avoid trying failed approaches again, attempt to change your thinking, not limited to using more advanced Python syntax like for, if, else, etc., or modifying the Prompt part"
else:
experience = f"No experience data found for round {current_round}."
graph_input = GRAPH_INPUT.format(experinece=experience, score=score, graph=graph[0], prompt=prompt)
node_prompt = GRAPH_OPTIMIZE_PROMPT + graph_input # TODO 看一眼谁先谁后这个地方
node = await ActionNode.from_pydantic(GraphOptimize).fill(
context=node_prompt, mode="context_fill", llm=self.llm
)
max_retries = 5
retries = 0
while retries < max_retries:
try:
# TODO 需要和评测的模型分开传入模型或其它方法如果能实现Temperature调整更好
response = node.instruct_content.model_dump()
break
except Exception as e:
retries += 1
print(f"Error generating prediction: {e}. Retrying... ({retries}/{max_retries})")
if retries == max_retries:
print("Maximum retries reached. Skipping this sample.")
break
time.sleep(5)
# TODO 这里其实可以省去
graph_match = response["graph"]
prompt_match = response["prompt"]
modification_match = response["modification"]
modification = modification_match.group(1)
prompt = prompt_match.group(1)
graph = GRAPH_TEMPLATE.format(graph=graph_match.group(1), round=self.round + 1)
# 将 graph.py 文件写入到目录中
with open(os.path.join(directory, "graph.py"), "w", encoding="utf-8") as file:
file.write(graph)
# 将 prompt.py 文件写入到目录中
with open(os.path.join(directory, "prompt.py"), "w", encoding="utf-8") as file:
file.write(prompt)
# 将 prompt.py 文件写入到目录中
with open(os.path.join(directory, "__init__.py"), "w", encoding="utf-8") as file:
file.write("")
experience = {
"father node": sample["round"],
"modification": modification,
"before": sample["score"],
"after": None,
"succeed": None,
}
with open(os.path.join(directory, "experience.json"), "w", encoding="utf-8") as file:
json.dump(experience, file, ensure_ascii=False, indent=4)
score = evaluator.validation_evaluate(self.dataset, self.graph)
experience["after"] = score

View file

@ -22,14 +22,50 @@ INITIALIZE_OPERATOR_PROMPT = """
# TODO 这里也需要自适应的完成针对不同数据集的GRAPH OPTIMIZE PROMPT
GRAPH_OPTIMIZE_PROMPT = """You are building a Graph and corresponding Prompt to jointly solve mathematical problems.
Referring to the given combination of graph and prompt, which forms a basic example of a mathematical solution approach, please reconstruct and optimize the Prompt and Graph. You can add, modify, or delete nodes and parameters in the graph, as well as modify, delete, or add new Prompts.
GRAPH_OPTIMIZE_PROMPT = """You are building a Graph and corresponding Prompt to jointly solve {type} problems.
Referring to the given combination of graph and prompt, which forms a basic example of a {type} solution approach, please reconstruct and optimize the Prompt and Graph. You can add, modify, or delete nodes and parameters in the graph, as well as modify, delete, or add new Prompts.
Put your modification (only make one point of change, i.e., one sentence), and the modified Prompt and Graph in XML tags in your reply. They will be used as new Prompt and Graph for calculation and iteration. Please ensure they are complete and correct, otherwise it may lead to runtime failures.
Only modify the parts in Prompt and Graph within /async def __call__(self, problem: str):/, otherwise it will cause parsing failure.
Reply format (must be strictly followed) (do not include any other formats except for the given XML format):
<modification>You should fill in the details of your modifications here, to facilitate future review.</modification>
<graph>graph</graph>
<prompt>prompt</prompt>
When optimizing, you can refer to critical thinking, and can incorporate methods such as Review, Revise, Ensemble, selfAsk, etc. Don't be limited to the previous format.You can consider Python's built-in loops (like for, while, and list comprehensions) or conditional statements (such as if-elif-else and ternary operators), or even machine learning methods ranging from basic supervised learning techniques (e.g., linear regression, decision trees) to more advanced approaches like neural networks and clustering algorithms. However, you must ensure that each call to the Graph internally involves at most 10 interactions, i.e., the complexity of the graph does not exceed 15."""
GRAPH_INPUT = """
Here is a Graph and corresponding Prompt that performed excellently in a previous iteration (maximum score is 1):\n
<sample>
<experience>{experience}</experience>
<modification>None</modification>
<score>{score}</score>
<graph>{graph}</graph>
<prompt>{prompt}</prompt>
</sample>
First provide optimization ideas. Note that ANSWER_FORMAT_PROMPT must exist and cannot be modified. Only add/modify/delete one detail point, extensive modifications are prohibited.\n\n"
"""
GRAPH_TEMPLATE = """import os
from agentG.llm import LLM
import logging
from agentG.graphs.gsm8k.round_{round}.prompt import *
from logging.handlers import RotatingFileHandler
# 获取项目的根目录
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
LOG_FILE = os.path.join(ROOT_DIR, 'app.log')
# 创建一个RotatingFileHandler
file_handler = RotatingFileHandler(LOG_FILE, maxBytes=1024*1024, backupCount=5, encoding='utf-8')
file_handler.setLevel(logging.INFO)
# 创建一个格式化器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
# 配置根日志记录器
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[file_handler])
# 获取当前模块的logger
logger = logging.getLogger(__name__)
{graph}
"""

View file

@ -9,6 +9,7 @@ NOTE: You should use typing.List instead of list to do type annotation. Because
we can use typing to extract the type of the node, but we cannot use built-in list to extract.
"""
import json
import re
import typing
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple, Type, Union
@ -482,14 +483,34 @@ class ActionNode:
# If there are multiple fields, we might want to use self.key to find the right one
return self.key
def get_field_names(self):
"""
Get the field names from the Pydantic model associated with this ActionNode.
"""
model_class = self.create_class()
return model_class.model_fields.keys()
def xml_compile(self, context):
pass
field_names = self.get_field_names()
# Construct the example using the field names
examples = []
for field_name in field_names:
examples.append(f"<{field_name}>content</{field_name}>")
# Join all examples into a single string
example_str = "\n".join(examples)
# Add the example to the context
context += f"""
### format example (must be strictly followed) (do not include any other formats except for the given XML format)
{example_str}
"""
print(context)
return context
async def code_fill(self, context, function_name=None, timeout=USE_CONFIG_TIMEOUT):
"""
fill CodeBlock Node
Fill CodeBlock Using ``` ```
"""
field_name = self.get_field_name()
prompt = context
content = await self.llm.aask(prompt, timeout=timeout)
@ -499,9 +520,19 @@ class ActionNode:
async def context_fill(self, context):
"""
这个地方的代码实现的目的是
Fill Context with XML TAG
"""
pass
field_names = self.get_field_names()
extracted_data = {}
content = await self.llm.aask(context)
for field_name in field_names:
# Use regex to find content within XML tags matching the field name
pattern = rf"<{field_name}>(.*?)</{field_name}>"
match = re.search(pattern, content, re.DOTALL)
if match:
extracted_data[field_name] = match.group(1).strip()
return extracted_data
async def fill(
self,
@ -550,7 +581,7 @@ class ActionNode:
使用xml_compile但是这个版本没有办法实现system message temperature
"""
context = self.xml_compile(context=self.context)
result = await self.context_fill(context, timeout)
result = await self.context_fill(context)
self.instruct_content = self.create_class()(**result)
return self

6
optimize.py Normal file
View file

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# @Date : 8/23/2024 20:00 PM
# @Author : didi
# @Desc : Experiment of graph optimization
# TODO 实现两个LLM Config