fix: 修复花括号显示异常问题
This commit is contained in:
@@ -52,17 +52,25 @@ IMAGE_PLACEHOLDER_TEMPLATE = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 匹配任意层数的图片占位符(2/4/6/8...层花括号),整段替换为规范四层,避免多余花括号残留导致客户端显示异常
|
||||||
|
_IMAGE_PLACEHOLDER_ANY_BRACES_RE = re.compile(
|
||||||
|
r"(\{\{)+IMAGE:\s*([^}]+)(\}\})+",
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def inject_image_placeholder_template(content: str) -> str:
|
def inject_image_placeholder_template(content: str) -> str:
|
||||||
"""
|
"""
|
||||||
入库前对章节正文做占位符处理:用正则匹配所有图片占位符位置,拼上固定模板。
|
入库前对章节正文做占位符处理:用正则匹配所有图片占位符位置,拼上固定模板。
|
||||||
支持 {{IMAGE:...}} 与 {{{{IMAGE:...}}}} 两种格式,输出统一为四层大括号 + 固定模板 + 描述。
|
支持任意层数花括号(如 {{、{{{{、{{{{{{ 等),输出统一为四层大括号 + 固定模板 + 描述,
|
||||||
若占位符内已包含固定模板前缀则不再重复拼接,保证位置与逻辑与原先一致。
|
避免 LLM 输出花括号过多时只替换内层导致多余花括号残留在正文中、在手机端被原样显示。
|
||||||
|
若占位符内已包含固定模板前缀则不再重复拼接。
|
||||||
"""
|
"""
|
||||||
if not content or not content.strip():
|
if not content or not content.strip():
|
||||||
return content
|
return content
|
||||||
|
|
||||||
def replace_one(match: re.Match) -> str:
|
def replace_one(match: re.Match) -> str:
|
||||||
inner = match.group(1).strip()
|
inner = (match.group(2) or "").strip()
|
||||||
if not inner:
|
if not inner:
|
||||||
return match.group(0)
|
return match.group(0)
|
||||||
if inner.startswith(IMAGE_PLACEHOLDER_TEMPLATE):
|
if inner.startswith(IMAGE_PLACEHOLDER_TEMPLATE):
|
||||||
@@ -70,20 +78,7 @@ def inject_image_placeholder_template(content: str) -> str:
|
|||||||
return "{{{{IMAGE:" + IMAGE_PLACEHOLDER_TEMPLATE + ("。" + desc if desc else "") + "}}}}"
|
return "{{{{IMAGE:" + IMAGE_PLACEHOLDER_TEMPLATE + ("。" + desc if desc else "") + "}}}}"
|
||||||
return "{{{{IMAGE:" + IMAGE_PLACEHOLDER_TEMPLATE + "。" + inner + "}}}}"
|
return "{{{{IMAGE:" + IMAGE_PLACEHOLDER_TEMPLATE + "。" + inner + "}}}}"
|
||||||
|
|
||||||
# 四层大括号
|
content = _IMAGE_PLACEHOLDER_ANY_BRACES_RE.sub(replace_one, content)
|
||||||
content = re.sub(
|
|
||||||
r"\{\{\{\{IMAGE:\s*([^}]+)\}\}\}\}",
|
|
||||||
replace_one,
|
|
||||||
content,
|
|
||||||
flags=re.DOTALL,
|
|
||||||
)
|
|
||||||
# 两层大括号(兼容旧格式)
|
|
||||||
content = re.sub(
|
|
||||||
r"\{\{IMAGE:\s*([^}]+)\}\}",
|
|
||||||
replace_one,
|
|
||||||
content,
|
|
||||||
flags=re.DOTALL,
|
|
||||||
)
|
|
||||||
return content
|
return content
|
||||||
def get_system_prompt() -> str:
|
def get_system_prompt() -> str:
|
||||||
"""获取整理 Agent 的系统提示词"""
|
"""获取整理 Agent 的系统提示词"""
|
||||||
@@ -183,11 +178,12 @@ def get_text_rewrite_prompt(segments_text: str, chapter_category: str, existing_
|
|||||||
3. 如果已有章节内容,请将新内容与已有内容自然融合
|
3. 如果已有章节内容,请将新内容与已有内容自然融合
|
||||||
4. 在内容中适当位置插入图片占位符
|
4. 在内容中适当位置插入图片占位符
|
||||||
|
|
||||||
## 图片占位符格式
|
## 图片占位符格式(必须严格遵守)
|
||||||
在描述场景、人物、重要时刻的段落后,插入占位符,格式为:{{{{IMAGE:具体的图片描述}}}}
|
- **唯一合法格式**:开头恰好四个左花括号、结尾恰好四个右花括号,中间为 IMAGE:具体描述。即:{{{{IMAGE:具体的图片描述}}}}
|
||||||
占位符单独占一行,描述要具体、有画面感。系统会在入库前自动拼上统一风格模板,你只需写场景描述即可。
|
- 禁止使用两层 {{ }}、六层 {{{{{{ }}}}}} 或任意其它层数,否则会在手机端显示异常。
|
||||||
|
- 占位符单独占一行,描述要具体、有画面感。系统会在入库前自动拼上统一风格模板,你只需写场景描述即可。
|
||||||
|
|
||||||
示例:
|
正确示例(仅此格式):
|
||||||
{{{{IMAGE:南方小镇的青石板路,两旁是白墙黑瓦的老房子}}}}
|
{{{{IMAGE:南方小镇的青石板路,两旁是白墙黑瓦的老房子}}}}
|
||||||
{{{{IMAGE:奶奶坐在院子里的藤椅上,手里摇着蒲扇}}}}"""
|
{{{{IMAGE:奶奶坐在院子里的藤椅上,手里摇着蒲扇}}}}"""
|
||||||
|
|
||||||
@@ -344,11 +340,12 @@ def get_narrative_prompt(
|
|||||||
8. **不要将对话中的交互性语言(如"我跟你说"、"你知道吗")写入叙述**
|
8. **不要将对话中的交互性语言(如"我跟你说"、"你知道吗")写入叙述**
|
||||||
9. **不要在正文中插入章节标题或分类标签**(如"章节:信念与价值观"、"## 童年与成长背景"等),章节标题由系统单独管理
|
9. **不要在正文中插入章节标题或分类标签**(如"章节:信念与价值观"、"## 童年与成长背景"等),章节标题由系统单独管理
|
||||||
|
|
||||||
## 图片占位符格式
|
## 图片占位符格式(必须严格遵守)
|
||||||
在描述场景、人物、重要时刻的段落后,插入占位符,格式为:{{{{IMAGE:具体的图片描述}}}}
|
- **唯一合法格式**:开头恰好四个左花括号、结尾恰好四个右花括号,即:{{{{IMAGE:具体的图片描述}}}}
|
||||||
占位符单独占一行,描述要具体、有画面感。系统会在入库前自动拼上统一风格模板,你只需写场景描述即可。
|
- 禁止两层 {{ }}、六层 {{{{{{ }}}}}} 或其它层数,否则会在手机端显示多余花括号。
|
||||||
|
- 占位符单独占一行,描述要具体、有画面感。系统会在入库前自动拼上统一风格模板,你只需写场景描述即可。
|
||||||
|
|
||||||
示例:
|
正确示例(仅此格式):
|
||||||
- {{{{IMAGE:南方小镇的青石板路,两旁是白墙黑瓦的老房子}}}}
|
- {{{{IMAGE:南方小镇的青石板路,两旁是白墙黑瓦的老房子}}}}
|
||||||
- {{{{IMAGE:奶奶坐在院子里的藤椅上,手里摇着蒲扇}}}}
|
- {{{{IMAGE:奶奶坐在院子里的藤椅上,手里摇着蒲扇}}}}
|
||||||
- {{{{IMAGE:少年背着书包站在火车站台上,回望身后的小镇}}}}
|
- {{{{IMAGE:少年背着书包站在火车站台上,回望身后的小镇}}}}
|
||||||
@@ -359,6 +356,7 @@ def get_narrative_prompt(
|
|||||||
- 每 200-300 字左右可以插入一个
|
- 每 200-300 字左右可以插入一个
|
||||||
- 单独占一行,不要嵌入段落中
|
- 单独占一行,不要嵌入段落中
|
||||||
- 不要使用括号或星号等其他格式
|
- 不要使用括号或星号等其他格式
|
||||||
|
- **花括号必须且仅能为四层**:{{{{ 与 }}}} 各四个,不多不少
|
||||||
|
|
||||||
只输出新对话内容的改写结果(包含图片占位符)。如果对话中没有值得记录的人生经历内容,输出空字符串。
|
只输出新对话内容的改写结果(包含图片占位符)。如果对话中没有值得记录的人生经历内容,输出空字符串。
|
||||||
"""
|
"""
|
||||||
|
|||||||
51
api/tests/test_memory_prompts_inject.py
Normal file
51
api/tests/test_memory_prompts_inject.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
"""测试 memory_prompts.inject_image_placeholder_template:占位符花括号统一为四层,避免多余花括号残留"""
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from api.agents.prompts.memory_prompts import (
|
||||||
|
IMAGE_PLACEHOLDER_TEMPLATE,
|
||||||
|
inject_image_placeholder_template,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InjectImagePlaceholderTemplateTest(unittest.TestCase):
|
||||||
|
def test_normalizes_double_brace_to_four(self):
|
||||||
|
content = "段落。\n\n{{IMAGE:南方小镇的青石板路}}\n\n结尾。"
|
||||||
|
out = inject_image_placeholder_template(content)
|
||||||
|
self.assertIn("{{{{IMAGE:", out)
|
||||||
|
self.assertIn("}}}}", out)
|
||||||
|
# 应为四层占位符,且「结尾。」前不应有多余的 }}
|
||||||
|
self.assertIn("\n\n结尾。", out)
|
||||||
|
self.assertEqual(out.count("}}}}"), 1)
|
||||||
|
|
||||||
|
def test_normalizes_quad_brace_unchanged(self):
|
||||||
|
content = "段落。\n\n{{{{IMAGE:南方小镇的青石板路}}}}\n\n结尾。"
|
||||||
|
out = inject_image_placeholder_template(content)
|
||||||
|
self.assertEqual(out.count("{{{{"), out.count("}}}}"))
|
||||||
|
self.assertIn("{{{{IMAGE:", out)
|
||||||
|
self.assertNotRegex(out, r"\}\}\}\}\}\}") # 不应出现五层及以上闭合括号
|
||||||
|
|
||||||
|
def test_normalizes_six_braces_to_four_so_no_residue(self):
|
||||||
|
# LLM 有时会多打花括号,导致客户端按四层占位符 split 后残留 "{{" "}}" 被显示
|
||||||
|
content = "段落。\n\n{{{{{{IMAGE:南方小镇的青石板路}}}}}}\n\n结尾。"
|
||||||
|
out = inject_image_placeholder_template(content)
|
||||||
|
# 整段应被替换为四层,不应留下多余的 "{{" 或 "}}"
|
||||||
|
self.assertIn("{{{{IMAGE:" + IMAGE_PLACEHOLDER_TEMPLATE, out)
|
||||||
|
self.assertIn("}}}}", out)
|
||||||
|
self.assertNotIn("{{{{{{", out)
|
||||||
|
self.assertNotIn("}}}}}}", out)
|
||||||
|
# 正文前后不应出现裸花括号
|
||||||
|
parts = out.split("}}}}")
|
||||||
|
for i, p in enumerate(parts):
|
||||||
|
if "南方小镇" in p or i == 0:
|
||||||
|
continue
|
||||||
|
self.assertNotRegex(p, r"^\s*\{\{", msg=f"残留开括号 in part: {p!r}")
|
||||||
|
before, sep, after = out.partition("{{{{IMAGE:")
|
||||||
|
self.assertNotRegex(before, r"\{\{\s*$", msg="开头段不应以 {{ 结尾")
|
||||||
|
self.assertNotRegex(after, r"^\s*\}\}", msg="占位符后不应以 }} 开头")
|
||||||
|
|
||||||
|
def test_normalizes_eight_braces_to_four(self):
|
||||||
|
content = "前\n\n{{{{{{{{IMAGE:奶奶的藤椅}}}}}}}}\n\n后"
|
||||||
|
out = inject_image_placeholder_template(content)
|
||||||
|
self.assertIn("{{{{IMAGE:", out)
|
||||||
|
self.assertNotIn("{{{{{{{{", out)
|
||||||
|
self.assertNotIn("}}}}}}}}", out)
|
||||||
Reference in New Issue
Block a user