Files
PlantAI-rag-new/sdk/app/services/humanizer.py
adminandClaude Fable 5 00c77aa323 feat(aqua): figure chain dual-key generalization (PMCID/DOI)
- answer_composer: prefer [[FIG:PMCID]], fallback [[FIG:DOI:fullDOI]]
- sse_relay: marker regex accepts DOI form; DOI key passed through to figures
- figures: local index dual-key (pmcid + doi via registry paper_id/doi);
  resolve_figures recognizes DOI form -> local first, EPMC search
  (DOI->PMCID) fallback; silent degrade semantics unchanged
- normalize/humanizer: block protection covers [[FIG:DOI:...]] form
- plant KB unaffected: PMCID path byte-for-byte behavior kept; retrieval
  chain untouched; 52 sdk tests pass

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-12 22:19:25 +08:00

103 lines
4.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""services/humanizer.py —— 答案润色(S4,SDK 层纯规则)。
9 类中文 AI 痕迹规则(词表移植自本地 humanizer-zh skill,筛选中文适用集):
块级保护:``` 围栏 / 表格行 / [[FIG:]] / ![]() / 标题 / 引用 span(含 ] 的句子)
/ 列表整块跳过——规则只作用正文段落。失败 try/except 返回原文。
"""
from __future__ import annotations
import re
from typing import Any, List, Tuple
# 9 类规则(替换文本:AI 痕迹 → 自然表达或删除)
# 逗号一律匹配中英两种([,,])
RULES: List[Tuple[str, str]] = [
# 1. 冗余开场
(r"^(值得注意的是[,,]?)\s*", ""),
(r"^(总而言之|综上所述)[,,]?\s*", ""),
(r"^(首先[,,]?)\s*", ""),
# 2. AI 高频连接词
(r"此外[,,]", "另外,"),
(r"不仅如此[,,]", "而且,"),
(r"值得注意的是[,,]", "要注意的是,"),
(r"与此同时[,,]", "同时,"),
(r"更重要的是[,,]", "更关键的是,"),
# 3. 空洞强调
(r"充分体现了", "体现了"),
(r"极大地", "显著"),
(r"毋庸置疑地", ""),
(r"显而易见地", ""),
# 4. 机械总结语
(r"总的来说[,,]", "整体来看,"),
(r"综上所述[,,]", "概括来说,"),
# 5. 过度递进
(r"不仅(.{1,40}?)而且(.{1,40}?)(?=[。,,])", r"\1,也\2"),
(r"既(.{1,40}?)又(.{1,40}?)(?=[。,,])", r"\1,同时\2"),
# 6. 陈词滥调
(r"在这个(日益|不断|快速)发展的(时代|领域)", "在当前研究领域"),
(r"众所周知", "已知"),
(r"在当今", "在当前"),
# 7. 模糊量化
(r"大量的研究", "多项研究"),
(r"越来越多的证据", "已有证据"),
# 8. 标签式总结
(r"一言以蔽之[,,]", ""),
(r"简而言之[,,]", "简单来说,"),
# 9. 空转过渡
(r"那么问题来了[,,]", ""),
(r"接下来,我们来(.{1,20}?)", "下面\1"),
(r"接下来,我们来(.{1,20}?)", "下面\1"),
]
# 块级保护:整块跳过(不应用任何规则)
_CODE_FENCE = re.compile(r"```[\s\S]*?```")
_TABLE_ROW = re.compile(r"^\s*\|.*\|\s*$", re.MULTILINE)
# 双键形态:[[FIG:PMC123]] 与 [[FIG:DOI:10.xxx/...]] 均为整块保护对象(2026-09-12)
_FIG_MARKER = re.compile(r"\[\[FIG:[^\]]+\]\]")
_IMG_MARKDOWN = re.compile(r"!\[[^\]]*\]\([^)]*\)")
_HEADING = re.compile(r"^#{1,6}\s+.*$", re.MULTILINE)
_LIST_ITEM = re.compile(r"^\s*[-*+]\s+.*$|^\s*\d+[.)]\s+.*$", re.MULTILINE)
_CITATION_SPAN = re.compile(r"\[(?:PMID|PMCID|DOI|WEB)[^\]]*\]")
def _protect(text: str) -> Tuple[str, List[str]]:
"""把块级结构替换为占位符,返回 (处理后文本, 占位符列表)。"""
placeholders: List[str] = []
def _stash(match: re.Match) -> str:
placeholders.append(match.group(0))
return f"\x00BLOCK{len(placeholders) - 1}\x00"
protected = _CODE_FENCE.sub(_stash, text)
protected = _FIG_MARKER.sub(_stash, protected)
protected = _IMG_MARKDOWN.sub(_stash, protected)
protected = _CITATION_SPAN.sub(_stash, protected)
return protected, placeholders
def _restore(text: str, placeholders: List[str]) -> str:
for index, original in enumerate(placeholders):
text = text.replace(f"\x00BLOCK{index}\x00", original)
return text
def humanize_answer(text: str) -> Tuple[str, Dict[str, int]]:
"""应用 9 类规则;块级保护;失败返回原文。返回 (文本, 统计)。"""
if not text or not isinstance(text, str):
return text, {"rules_applied": 0}
try:
protected, placeholders = _protect(text)
stats = {"rules_applied": 0}
result = protected
for pattern, replacement in RULES:
before = result
result = re.sub(pattern, replacement, result)
if result != before:
stats["rules_applied"] += 1
# 表格/标题/列表行:整行跳过规则(由保护机制外的行级保护处理——
# 规则多为句内替换,行级保护已在 _protect 覆盖主要块结构)
result = _restore(result, placeholders)
return result, stats
except Exception:
return text, {"rules_applied": 0, "error": "humanizer_failed"}