Files
Wechat-Bot/wechat-bot/bot/reply_parser.py

137 lines
4.5 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.
"""
回复文本解析模块
负责处理 Agent 返回的原始文本,分离思考内容和正文。
SSE 实际输出格式(从日志确认):
- " thinking\\n...思考内容...\\n response\\n...正文内容..." (纯文本标记)
- 思考部分: " thinking\\n" 开头
- 正文部分: "\\n response\\n" 之后的所有内容
多轮调用时 full_text 会累加,调用方需要自行切分轮次。
"""
import re
# 思考-正文分隔标记:出现在文本中时,之前是思考,之后是正文
_RESPONSE_MARKER = re.compile(r'\n response\n')
def parse_reply(text: str) -> tuple[str, str]:
"""
从文本中分离思考内容和正文。
支持两种格式:
1. " thinking\\n...\\n response\\n..." SSE 实际纯文本格式)
2. " thinking... response" HTML 标签风格)
返回: (思考内容, 正文内容)
"""
if not text:
return "", ""
think_content = ""
body_content = text
# 格式1匹配 " thinking\\n ... \\n response\\n" 纯文本标记
# 从 SSE 日志确认: 思考内容以 "\\n response\\n" 结束
parts = _RESPONSE_MARKER.split(text, maxsplit=1)
if len(parts) == 2:
think_part, body_part = parts
# 去掉开头的 " thinking\\n"
think_part = think_part.strip()
if think_part.startswith(' thinking'):
think_part = think_part[len(' thinking'):].lstrip('\n')
think_content = think_part.strip()
body_content = body_part.strip()
# 格式2匹配 " thinking... response" HTML 标签风格(兜底)
if not think_content and "<think" in text:
match = re.search(r"<think([\s\S]*?)</think\s*>", text)
if match:
think_content = match.group(1).strip()
body_content = re.sub(r"<think[\s\S]*?</think\s*>", "", text).strip()
else:
think_content = text.replace("<think", "").strip()
body_content = ""
# 去重
body_content = _deduplicate(body_content)
think_content = _deduplicate(think_content)
return think_content, body_content
def extract_body_only(text: str) -> str:
"""
从文本中只提取正文内容(不含思考)。
用于流式更新时过滤思考内容。
如果文本中还没有出现 "\\n response\\n" 标记,
说明还在思考阶段,返回空字符串避免把思考内容当正文发送。
"""
if not text:
return ""
# 检查正文标记是否已出现
split_pos = _find_response_marker(text)
if split_pos < 0:
# 还没出现 response 标记,仍在思考阶段,不返回任何内容
return ""
# 返回 response 之后的内容
body = text[split_pos + 1:]
# 去掉开头的 response\n
body = body.lstrip('\n')
if body.startswith(' response'):
body = body[len(' response'):].lstrip('\n')
# 再次检查是否有新的 response 标记(多轮场景)
pos2 = _find_response_marker(body)
if pos2 >= 0:
body = body[pos2 + 1:]
body = body.lstrip('\n')
if body.startswith(' response'):
body = body[len(' response'):].lstrip('\n')
return body.strip()
def _find_response_marker(text: str) -> int:
"""查找 \\n response\\n 标记的位置,返回 response 中 'r' 的索引"""
m = _RESPONSE_MARKER.search(text)
return m.start() if m else -1
def _deduplicate(text: str) -> str:
"""对回复文本去重。"""
if not text:
return text
# 行级去重
lines = text.split("\n")
non_empty = [l for l in lines if l.strip()]
if len(non_empty) > 1:
mid = len(non_empty) // 2
if non_empty[:mid] == non_empty[mid:]:
return "\n".join(non_empty[:mid])
for overlap_len in range(min(len(non_empty) // 2, 20), 0, -1):
if non_empty[-overlap_len:] == non_empty[:overlap_len]:
return "\n".join(non_empty[overlap_len:])
# 字符级去重
length = len(text)
for sub_len in range(length // 2, 0, -1):
if length % sub_len == 0 and text[:sub_len] * (length // sub_len) == text:
return text[:sub_len]
# 部分重叠
for overlap in range(min(length // 2, 500), 0, -1):
if text[:overlap] == text[-overlap:]:
trimmed = text[overlap:]
trimmed_len = len(trimmed)
for sub_len in range(trimmed_len // 2, 0, -1):
if trimmed_len % sub_len == 0 and trimmed[:sub_len] * (trimmed_len // sub_len) == trimmed:
return trimmed[:sub_len]
return text