""" 回复文本解析模块 负责处理 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 "", text) if match: think_content = match.group(1).strip() body_content = re.sub(r"", "", text).strip() else: think_content = text.replace(" 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