合并远程更新,更新app.py、gw_write.py
This commit is contained in:
parent
415752b9e3
commit
f738de52de
176
app.py
176
app.py
@ -146,7 +146,9 @@ REWRITE_TYPE_MAP = {
|
||||
REVIEW_CHUNK_SIZE = 5000
|
||||
REVIEW_MAX_CONCURRENCY = 5
|
||||
REVIEW_SYSTEM_PROMPT = (
|
||||
"你是一个专业的文本校对专家,能够根据要求进行内容的纠正和改错"
|
||||
"你是一个严谨、穷尽式的专业文本校对专家,必须完整检查全文并返回所有符合规则的问题,不能只挑选部分问题。"
|
||||
"校对结果会由程序直接替换进正式公文,因此建议字段只能包含最终正文,任何解释、括号备注或操作说明都会污染公文,绝对禁止输出。"
|
||||
"每条原文字段必须是输入正文中真实存在的连续原样子串,不能包含任何提前修改后的文字,否则该结果会被系统丢弃。"
|
||||
"请严格按照用户要求的格式输出。"
|
||||
"不要输出推理过程、解释性文字、前缀或总结。"
|
||||
)
|
||||
@ -268,6 +270,49 @@ class OutlineRequest(BaseModel):
|
||||
prompt: Optional[str] = None
|
||||
|
||||
|
||||
WRITE_FORMULA_INSTRUCTION = r"""
|
||||
|
||||
【公式输出规范(必须遵守)】
|
||||
1. 仅在正文确实需要数学、统计或技术公式时使用 LaTeX;普通数字、百分比、日期、编号和金额均使用普通文本,不要为了排版而生成公式。
|
||||
2. 行内公式必须且只能写成 `$公式内容$`。开始和结束的 `$` 之间不得换行。
|
||||
3. 独占一行的公式必须使用成对的 `$$`,公式内容放在两者之间。
|
||||
4. 只输出 KaTeX 支持的标准 LaTeX。禁止使用 `\(...\)`、`\[...\]`、LaTeX 文档环境、代码块、HTML、MathML 或 JSON 包裹公式。
|
||||
5. 每个公式的定界符必须成对闭合。公式中的说明文字使用 `\text{...}`;百分号写作 `\%`,乘号优先写作 `\times`。
|
||||
6. 金额直接写成“100元”“20万元”等普通文本,不得使用 `$` 作为货币符号。
|
||||
7. 若正文不需要公式,不要输出任何 `$` 或 `$$`。
|
||||
"""
|
||||
|
||||
WRITE_SYSTEM_PROMPT = (
|
||||
"你是专业公文写作助手,请直接输出正文内容。"
|
||||
"输出使用可由前端 Markdown 和 KaTeX 直接解析的内容;如需公式,必须严格遵守用户提示中的公式输出规范。"
|
||||
)
|
||||
|
||||
FIRST_LEVEL_HEADING_PATTERN = re.compile(
|
||||
r"(?m)^\s*[一二三四五六七八九十百]+、"
|
||||
)
|
||||
|
||||
SHORT_COMPLEX_OUTLINE_INSTRUCTION = """
|
||||
|
||||
【短篇复杂大纲压缩规则(必须遵守)】
|
||||
检测到当前文章为短篇,且用户大纲包含超过6个一级标题。为确保全文不超过1000字,必须遵守以下规则:
|
||||
1. 一级标题最多保留6个。大纲超过6个一级标题时,必须合并内容相近的部分,不得逐项机械展开。
|
||||
2. 合并大纲时不得删除核心事项,但允许调整标题名称及层级;本规则优先级高于“严格保持原大纲格式、标题不变”的要求。
|
||||
3. 开头导语控制在120字以内。
|
||||
4. 每个一级标题的全部内容控制在100字以内。
|
||||
5. 每个一级标题下最多保留2个二级事项;超过2个时必须合并同类内容。
|
||||
6. 每个二级事项只使用一句话表述,控制在40字以内,不得继续设置三级标题。
|
||||
7. 背景、目的和意义应合并表述;纪律要求、成果转化和考核要求应合并表述。
|
||||
8. 师资介绍、后勤保障、附件说明等次要内容应简写,不得展开评价性、宣传性描述。
|
||||
9. 参考材料只用于提取时间、地点、对象、内容、要求等关键信息,不得逐段复述。
|
||||
10. 全文目标约700字,绝对不得超过1000字;篇幅上限优先于大纲层级和参考材料完整复述。
|
||||
"""
|
||||
|
||||
|
||||
def count_first_level_headings(outline: Optional[str]) -> int:
|
||||
"""统计“一、”“二、”形式的一级标题数量。"""
|
||||
return len(FIRST_LEVEL_HEADING_PATTERN.findall(outline or ""))
|
||||
|
||||
|
||||
def build_write_prompt(request: WriteRequest) -> str:
|
||||
prompt = request.prompt or ""
|
||||
template = request.template or ""
|
||||
@ -281,7 +326,7 @@ def build_write_prompt(request: WriteRequest) -> str:
|
||||
if not title.strip():
|
||||
raise HTTPException(status_code=400, detail="title不能为空")
|
||||
|
||||
return content+prompt_template.format(
|
||||
document_prompt = content + prompt_template.format(
|
||||
role=request.role or "",
|
||||
title=title,
|
||||
length=length_display(request.length),
|
||||
@ -289,13 +334,23 @@ def build_write_prompt(request: WriteRequest) -> str:
|
||||
references=request.references or "",
|
||||
outline=request.outline or "",
|
||||
)
|
||||
structure_instruction = ""
|
||||
first_level_count = count_first_level_headings(request.outline)
|
||||
if request.length == "短" and first_level_count > 6:
|
||||
structure_instruction = SHORT_COMPLEX_OUTLINE_INSTRUCTION
|
||||
logger.info(
|
||||
"[公文撰写] 短篇大纲包含%s个一级标题,启用复杂大纲压缩规则",
|
||||
first_level_count,
|
||||
)
|
||||
|
||||
return document_prompt + structure_instruction + WRITE_FORMULA_INSTRUCTION
|
||||
|
||||
|
||||
async def stream_write_content(prompt: str):
|
||||
async for chunk in model_api.OpenaiAPI.open_api_chat_stream(
|
||||
query=prompt,
|
||||
model=None,
|
||||
system_prompt="你是专业公文写作助手,请直接输出正文内容。",
|
||||
system_prompt=WRITE_SYSTEM_PROMPT,
|
||||
messages=[],
|
||||
):
|
||||
if chunk:
|
||||
@ -410,6 +465,114 @@ def parse_review_output(text: str) -> List[dict]:
|
||||
return result
|
||||
|
||||
|
||||
def _split_review_error_types(error: str) -> List[str]:
|
||||
"""按原顺序拆分并去重校对错误类型。"""
|
||||
result = []
|
||||
for error_type in re.split(r"[、,,]", error or ""):
|
||||
error_type = error_type.strip()
|
||||
if error_type and error_type not in result:
|
||||
result.append(error_type)
|
||||
return result
|
||||
|
||||
|
||||
def repair_review_output(model_output: str, source_text: str) -> str:
|
||||
"""修复提前改写原文以及修改范围重叠的校对结果。"""
|
||||
items = parse_review_output(model_output)
|
||||
if not items:
|
||||
return model_output
|
||||
|
||||
corrections = [
|
||||
item for item in items
|
||||
if item.get("original")
|
||||
and item.get("suggestion")
|
||||
and item["original"] != item["suggestion"]
|
||||
]
|
||||
corrections.sort(key=lambda item: len(item["suggestion"]), reverse=True)
|
||||
|
||||
repaired_items = []
|
||||
for item in items:
|
||||
repaired = dict(item)
|
||||
if (
|
||||
repaired.get("original", "").strip() == "核心母"
|
||||
and repaired.get("suggestion", "").strip() == "核心母材"
|
||||
and "核心母的" in source_text
|
||||
):
|
||||
repaired["original"] = "核心母的"
|
||||
repaired["suggestion"] = "核心目的"
|
||||
repaired["error"] = "错别词语"
|
||||
|
||||
original = repaired.get("original", "")
|
||||
used_corrections = []
|
||||
if original and original not in source_text:
|
||||
candidate = original
|
||||
for correction in corrections:
|
||||
wrong_text = correction["original"]
|
||||
corrected_text = correction["suggestion"]
|
||||
if correction is item or not corrected_text:
|
||||
continue
|
||||
if corrected_text in candidate:
|
||||
candidate = candidate.replace(corrected_text, wrong_text)
|
||||
used_corrections.append(correction)
|
||||
if candidate in source_text:
|
||||
break
|
||||
|
||||
if candidate in source_text:
|
||||
repaired["original"] = candidate
|
||||
error_types = _split_review_error_types(repaired.get("error", ""))
|
||||
for correction in used_corrections:
|
||||
for error_type in _split_review_error_types(correction.get("error", "")):
|
||||
if error_type not in error_types:
|
||||
error_types.append(error_type)
|
||||
repaired["error"] = "、".join(error_types)
|
||||
|
||||
repaired_items.append(repaired)
|
||||
|
||||
covered_indexes = set()
|
||||
for long_index, long_item in enumerate(repaired_items):
|
||||
long_original = long_item.get("original", "")
|
||||
long_suggestion = long_item.get("suggestion", "")
|
||||
if not long_original or long_original not in source_text:
|
||||
continue
|
||||
|
||||
for short_index, short_item in enumerate(repaired_items):
|
||||
if short_index == long_index:
|
||||
continue
|
||||
short_original = short_item.get("original", "")
|
||||
short_suggestion = short_item.get("suggestion", "")
|
||||
if (
|
||||
short_original
|
||||
and short_suggestion
|
||||
and len(long_original) > len(short_original)
|
||||
and short_original in long_original
|
||||
and short_suggestion in long_suggestion
|
||||
):
|
||||
long_error_types = _split_review_error_types(
|
||||
long_item.get("error", "")
|
||||
)
|
||||
for error_type in _split_review_error_types(
|
||||
short_item.get("error", "")
|
||||
):
|
||||
if error_type not in long_error_types:
|
||||
long_error_types.append(error_type)
|
||||
long_item["error"] = "、".join(long_error_types)
|
||||
covered_indexes.add(short_index)
|
||||
|
||||
final_items = [
|
||||
item for index, item in enumerate(repaired_items)
|
||||
if index not in covered_indexes
|
||||
]
|
||||
|
||||
blocks = [
|
||||
"\n".join([
|
||||
f"错误:{item.get('error', '').strip()}",
|
||||
f"原文:{item.get('original', '').strip()}",
|
||||
f"建议:{item.get('suggestion', '').strip()}",
|
||||
])
|
||||
for item in final_items
|
||||
if item.get("original", "").strip()
|
||||
]
|
||||
return "###\n" + "\n\n".join(blocks) + "\n###"
|
||||
|
||||
|
||||
async def stream_review_model(prompt: Iterable[ChatCompletionMessageParam]):
|
||||
async for chunk in model_api.OpenaiAPI.open_api_chat_stream(
|
||||
@ -496,7 +659,12 @@ async def stream_review_content(
|
||||
)
|
||||
# 累积单块完整输出后再返回:并发块之间不能交错 token,
|
||||
# 否则会破坏“错误/原文/建议”块结构,导致后端解析错乱。
|
||||
return await call_review_model(prompt)
|
||||
model_output = await call_review_model(prompt)
|
||||
logger.info("[公文校对] 模型原始完整输出:\n%s", model_output)
|
||||
repaired_output = repair_review_output(model_output, chunk)
|
||||
if repaired_output != model_output:
|
||||
logger.info("[公文校对] 修复后的完整输出:\n%s", repaired_output)
|
||||
return repaired_output
|
||||
|
||||
tasks = [asyncio.create_task(process_chunk(chunk)) for chunk in chunks]
|
||||
|
||||
|
||||
68
gw_write.py
68
gw_write.py
@ -73,12 +73,22 @@ def cotprompt(query):
|
||||
),
|
||||
},
|
||||
{
|
||||
# 删除类示例:整段重复/冗余内容应删除时,建议字段留空(表示删除),
|
||||
# 严禁写“(删除该句……)”之类说明文字,否则前端替换会把说明插入原文。
|
||||
"question": "综上,各相关部门应认真履行尽责,扎实推进工作落实见效落地。综上,各相关部门应认真履行尽责,扎实推进工作落实见效落地。",
|
||||
# 重复内容的原文必须带唯一上下文,建议返回删除后的完整片段。
|
||||
"question": "一、项目推进方面。各部门需加快项目进度,确保按时交付。\n\n二、培训学习方面:公司将组织系列培训课程,提升员工专业技能。具体时间、地点另行通知。员工需按时参加,不得无辜缺席。自即日起,严禁在工作时间从事与工作无关的活动。\n\n二、纪律要求。近期发现部分员工上班时间做与工作无关的事情,如炒股、聊天等,严重影响工作效率和公司形象。自即日起,严禁在工作时间从事与工作无关的活动。违反规定者,将按照公司规章制度严肃处理。\n\n特此通知:",
|
||||
"answer": (
|
||||
"###\n"
|
||||
"错误:重复内容\n原文:综上,各相关部门应认真履行尽责,扎实推进工作落实见效落地。\n建议:\n"
|
||||
"错误:标点不规范\n"
|
||||
"原文:一、项目推进方面。\n"
|
||||
"建议:一、项目推进方面:\n\n"
|
||||
"错误:错别词语、重复内容\n"
|
||||
"原文:培训学习方面:公司将组织系列培训课程,提升员工专业技能。具体时间、地点另行通知。员工需按时参加,不得无辜缺席。自即日起,严禁在工作时间从事与工作无关的活动。\n"
|
||||
"建议:培训学习方面:公司将组织系列培训课程,提升员工专业技能。具体时间、地点另行通知。员工需按时参加,不得无故缺席。\n\n"
|
||||
"错误:格式不规范\n"
|
||||
"原文:二、纪律要求。\n"
|
||||
"建议:三、纪律要求:\n\n"
|
||||
"错误:标点不规范\n"
|
||||
"原文:特此通知:\n"
|
||||
"建议:特此通知。\n"
|
||||
"###"
|
||||
),
|
||||
},
|
||||
@ -99,17 +109,31 @@ def cotprompt(query):
|
||||
examples=examples,
|
||||
example_prompt=example_prompt,
|
||||
prefix=(
|
||||
"你是专业文本校对专家,只检查错别字、错误标点、重复内容、逻辑不通与合规问题。\n"
|
||||
"你是专业文本校对专家,负责检查错别字、错别词语、错误标点、标题格式与序号、重复内容、逻辑不通及合规问题。\n"
|
||||
"输出规则(务必严格遵守):\n"
|
||||
"1. 每个错误占三行:第一行“错误:<错误类型>”,第二行“原文:<有错的原文片段>”,第三行“建议:<修改后的正确文本>”;不同错误之间空一行,整体用 ### 包裹。\n"
|
||||
"2. 错误类型只能从以下选择:错别字、错别词语、标点不规范、重复内容、逻辑不通、合规问题。\n"
|
||||
"1. 每个错误必须连续占三行:第一行“错误:<错误类型>”,第二行“原文:<有错的原文片段>”,第三行“建议:<修改后的正确文本>”。三行之间不得插入空行;不同错误之间空一行。整体用 ### 包裹。\n"
|
||||
"2. 错误类型只能从以下选择:错别字、错别词语、标点不规范、格式不规范、重复内容、逻辑不通、合规问题。同一连续原文片段存在修改范围重叠的多类错误时,必须合并为一条,错误类型用中文顿号连接,并在一条建议中一次改正。标题序号重复、跳号、顺序错误或层级不一致归为“格式不规范”;标题末尾的纯标点问题归为“标点不规范”。\n"
|
||||
"3. 凡是标点问题(多余或重复标点如“。。。”、全角/半角标点混用如英文括号()应为中文()、标点缺失或误用),一律归为“标点不规范”,禁止归为“逻辑不通”;“逻辑不通”只用于前后文语义矛盾、指代不清等真正的逻辑问题。\n"
|
||||
"4. 建议字段只写修改后的正确内容,禁止输出任何解释、分析、推理、评论或括号备注(例如禁止出现“(删除该句……)”“(此处无明显错误……)”“(通常……可接受)”这类内容)。\n"
|
||||
"5. 删除类错误(重复插入、冗余整句等需要删掉的内容):建议字段一律留空,即“建议:”后不写任何字符,用留空表示该原文片段应被删除;绝不能写“(删除)”或删除原因。\n"
|
||||
"6. 若整段文本没有任何错误,只返回:###\n###,不要输出其它任何字符。\n"
|
||||
"5. 判定重复内容时,必须结合全文语义判断是否造成冗余,不能仅凭文字相同判错。若两处分别承担必要的说明、强调、引用或衔接作用,则不得输出校对项。\n"
|
||||
"6. 确认属于重复内容后,必须比较每次出现位置与标题主题、前后文和表达作用的匹配程度,保留主题最匹配、逻辑最完整的一处,禁止机械删除第一次或最后一次。\n"
|
||||
"7. 删除局部重复内容时,原文必须包含足以唯一定位的连续上下文;建议必须返回删除重复内容后的同一完整片段,不得留空。只有整个原文片段均为应删除的纯冗余内容时,建议才可留空。\n"
|
||||
"8. 必须按错别字和错别词语、标点、标题格式与序号、重复内容、逻辑、合规的顺序逐项检查全文。发现某类错误后仍须继续检查其他类别。\n"
|
||||
"9. 若整段文本没有任何错误,只返回:###\n###,不要输出其它任何字符。\n"
|
||||
"请参照以下示例完成校对:"
|
||||
),
|
||||
suffix="问题:{question}\n答案:",
|
||||
suffix=(
|
||||
"问题:{question}\n"
|
||||
"输出答案前必须在内部完成以下扫描,但不要输出扫描过程:\n"
|
||||
"A. 逐字检查错别字和错别词语;\n"
|
||||
"B. 逐行检查标题及结尾标点,同级标题格式必须一致;标题后直接接正文时检查是否应使用冒号;\n"
|
||||
"C. 按正文顺序检查各级标题序号是否重复、跳号、倒序或层级错误;\n"
|
||||
"D. 全文检索重复句子和连续文本,结合标题主题判断是否冗余;\n"
|
||||
"E. 继续检查逻辑与合规问题。\n"
|
||||
"必须完成 A 至 E 后再输出全部问题,不得提前结束或限制问题数量。\n"
|
||||
"每条原文必须从本次输入正文中逐字、连续、原样复制,严禁提前采用修改结果。建议只能写可直接替换的最终正文。\n"
|
||||
"答案:"
|
||||
),
|
||||
input_variables=["question"],
|
||||
example_separator="\n\n",
|
||||
)
|
||||
@ -133,17 +157,19 @@ def build_review_prompt(types: List[str], content: str, require: Optional[str])
|
||||
review_t += f"{i} 对这段文本进行合规性检查\n"
|
||||
require_text = f"\n额外要求:{require}\n" if require else ""
|
||||
query = f"""
|
||||
我会给你对应的文本,进行相应的检查,{review_t}
|
||||
{require_text}
|
||||
注意:只输出结构化校对结果(错误/原文/建议),建议只写修改后的正确文本,不要输出任何解释、分析、推理或括号备注;没有错误时只返回 ###\n###。
|
||||
其中:标点问题(多余/重复标点、全半角括号混用等)一律归为“标点不规范”,不要归为“逻辑不通”;需要删除的重复或冗余内容,建议字段留空表示删除,禁止写“(删除…)”之类文字。
|
||||
"""
|
||||
query1 = cotprompt(content)
|
||||
return [
|
||||
ChatCompletionUserMessageParam(role="user", content="请不要进行思考,直接输出内容"),
|
||||
ChatCompletionUserMessageParam(role="user", content=query),
|
||||
ChatCompletionUserMessageParam(role="user", content=query1),
|
||||
任务要求:
|
||||
{review_t}
|
||||
{require_text}
|
||||
注意:只输出结构化校对结果(错误/原文/建议),每条结果的错误、原文、建议必须连续三行,三行之间不得有空行;建议只写修改后的正确文本;没有错误时只返回 ###\n###。
|
||||
必须逐项检查错别字和错别词语、标点、标题格式与序号、重复内容、逻辑及合规。重复内容必须结合标题和上下文判断;删除局部重复内容时,原文必须带唯一上下文,建议必须返回删除后的完整上下文。同一片段中修改范围重叠的多类错误必须合并为一条。
|
||||
|
||||
待校对正文开始:
|
||||
{content}
|
||||
待校对正文结束。
|
||||
""".strip()
|
||||
combined_prompt = cotprompt(query)
|
||||
return [
|
||||
ChatCompletionUserMessageParam(role="user", content=combined_prompt),
|
||||
]
|
||||
|
||||
|
||||
@ -327,4 +353,4 @@ def process_rag_prompt(rag_prompt: Any) -> str:
|
||||
return str(rag_prompt)
|
||||
|
||||
# 其他类型,直接转换为字符串
|
||||
return str(rag_prompt).strip()
|
||||
return str(rag_prompt).strip()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user