From 5c0bb6fb4eddc46b16f0015cd35cef4e1b91bc96 Mon Sep 17 00:00:00 2001 From: XianYuDaXian Date: Thu, 5 Mar 2026 23:15:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A2=9E=E5=BC=BA=E6=80=9D=E8=80=83?= =?UTF-8?q?=E5=8C=BA=E5=A4=9A=20agent=20=E5=88=86=E7=BB=84=E5=B9=B6?= =?UTF-8?q?=E8=A1=A5=E9=BD=90=20rollout=20=E5=89=8D=E7=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/grok/services/chat.py | 26 ++++++--- app/static/public/js/chat.js | 86 +++++++++++++++++++++++++++--- 2 files changed, 98 insertions(+), 14 deletions(-) diff --git a/app/services/grok/services/chat.py b/app/services/grok/services/chat.py index e2da2fb..0ee6428 100644 --- a/app/services/grok/services/chat.py +++ b/app/services/grok/services/chat.py @@ -33,7 +33,7 @@ _CHAT_SEMAPHORE = None _CHAT_SEM_VALUE = None -def extract_tool_text(raw: str) -> str: +def extract_tool_text(raw: str, rollout_id: str = "") -> str: if not raw: return "" name_match = re.search( @@ -60,12 +60,13 @@ def extract_tool_text(raw: str) -> str: label = name text = args + prefix = f"[{rollout_id}]" if rollout_id else "" if name == "web_search": - label = "[WebSearch]" + label = f"{prefix}[WebSearch]" if isinstance(payload, dict): text = payload.get("query") or payload.get("q") or "" elif name == "search_images": - label = "[SearchImage]" + label = f"{prefix}[SearchImage]" if isinstance(payload, dict): text = ( payload.get("image_description") @@ -74,7 +75,7 @@ def extract_tool_text(raw: str) -> str: or "" ) elif name == "chatroom_send": - label = "[AgentThink]" + label = f"{prefix}[AgentThink]" if isinstance(payload, dict): text = payload.get("message") or "" @@ -415,6 +416,7 @@ class StreamProcessor(proc_base.BaseProcessor): def __init__(self, model: str, token: str = "", show_think: bool = None): super().__init__(model, token) self.response_id: str = None + self.rollout_id: str = "" self.fingerprint: str = "" self.think_opened: bool = False self.role_sent: bool = False @@ -444,7 +446,7 @@ class StreamProcessor(proc_base.BaseProcessor): return "".join(output_parts) end_pos = end_idx + len(end_tag) self._tool_usage_buffer += rest[:end_pos] - line = extract_tool_text(self._tool_usage_buffer) + line = extract_tool_text(self._tool_usage_buffer, self.rollout_id) if line: if output_parts and not output_parts[-1].endswith("\n"): output_parts[-1] += "\n" @@ -470,7 +472,7 @@ class StreamProcessor(proc_base.BaseProcessor): end_pos = end_idx + len(end_tag) raw_card = rest[start_idx:end_pos] - line = extract_tool_text(raw_card) + line = extract_tool_text(raw_card, self.rollout_id) if line: if output_parts and not output_parts[-1].endswith("\n"): output_parts[-1] += "\n" @@ -554,6 +556,8 @@ class StreamProcessor(proc_base.BaseProcessor): self.fingerprint = llm.get("modelHash", "") if rid := resp.get("responseId"): self.response_id = rid + if rid := resp.get("rolloutId"): + self.rollout_id = str(rid) if not self.role_sent: yield self._sse(role="assistant") @@ -697,11 +701,17 @@ class CollectProcessor(proc_base.BaseProcessor): result = content if "xai:tool_usage_card" in self.filter_tags: + rollout_id = "" + rollout_match = re.search( + r"(.*?)", result, flags=re.DOTALL + ) + if rollout_match: + rollout_id = rollout_match.group(1).strip() result = re.sub( r"]*>.*?", lambda match: ( - f"{extract_tool_text(match.group(0))}\n" - if extract_tool_text(match.group(0)) + f"{extract_tool_text(match.group(0), rollout_id)}\n" + if extract_tool_text(match.group(0), rollout_id) else "" ), result, diff --git a/app/static/public/js/chat.js b/app/static/public/js/chat.js index e5e2b2b..3fe7596 100644 --- a/app/static/public/js/chat.js +++ b/app/static/public/js/chat.js @@ -728,16 +728,29 @@ return parts; } - function parseRolloutBlocks(text) { + function parseRolloutBlocks(text, defaultId = 'General') { const lines = (text || '').split(/\r?\n/); const blocks = []; let current = null; for (const line of lines) { - const match = line.match(/^\s*\[([^\]]+)\]\[([^\]]+)\]\s*(.*)$/); - if (match) { + const matchDouble = line.match(/^\s*\[([^\]]+)\]\[([^\]]+)\]\s*(.*)$/); + if (matchDouble) { if (current) blocks.push(current); - current = { id: match[1], type: match[2], lines: [] }; - if (match[3]) current.lines.push(match[3]); + current = { id: matchDouble[1], type: matchDouble[2], lines: [] }; + if (matchDouble[3]) current.lines.push(matchDouble[3]); + continue; + } + const matchSingle = line.match(/^\s*\[([^\]]+)\]\s*(.*)$/); + if (matchSingle) { + const maybeType = String(matchSingle[1] || '').trim(); + if (/^(WebSearch|SearchImage|AgentThink)$/i.test(maybeType)) { + if (current) blocks.push(current); + current = { id: defaultId || 'General', type: maybeType, lines: [] }; + if (matchSingle[2]) current.lines.push(matchSingle[2]); + continue; + } + } + if (current && /^\s*\[[^\]]+\]\s*$/.test(line)) { continue; } if (current) { @@ -779,6 +792,56 @@ return sections; } + function splitBlocksIntoSyntheticAgents(blocks) { + const list = Array.isArray(blocks) ? blocks : []; + if (!list.length) return []; + + const ids = Array.from(new Set(list.map((b) => String(b.id || '').trim()).filter(Boolean))); + const nonGeneralIds = ids.filter((id) => !/^general$/i.test(id)); + + // 优先按 rollout id 分组,能更稳定地映射多 agent。 + if (nonGeneralIds.length >= 2) { + const groups = []; + const map = new Map(); + for (const block of list) { + const key = String(block.id || 'General'); + let group = map.get(key); + if (!group) { + group = { key, blocks: [] }; + map.set(key, group); + groups.push(group); + } + group.blocks.push(block); + } + return groups.map((group, idx) => ({ + title: idx === 0 ? 'Grok Leader' : `Agent ${idx}`, + blocks: group.blocks + })); + } + + // 兜底:按 AgentThink 作为阶段收束点切分。 + const sections = []; + let current = []; + for (const block of list) { + current.push(block); + if (/^agentthink$/i.test(String(block.type || '').trim())) { + sections.push(current); + current = []; + } + } + if (current.length) { + sections.push(current); + } + + if (sections.length <= 1) { + return [{ title: 'Grok Leader', blocks: list }]; + } + return sections.map((part, idx) => ({ + title: idx === 0 ? 'Grok Leader' : `Agent ${idx}`, + blocks: part + })); + } + function renderThinkContent(text, openAll) { const sections = parseAgentSections(text); if (!sections.length) { @@ -812,7 +875,18 @@ }; const agentBlocks = sections.map((section, idx) => { - const blocks = parseRolloutBlocks(section.lines.join('\n')); + const blocks = parseRolloutBlocks(section.lines.join('\n'), section.title || 'General'); + if (!section.title && blocks.length) { + const synthetic = splitBlocksIntoSyntheticAgents(blocks); + if (synthetic.length > 1) { + return synthetic.map((agent, agentIdx) => { + const inner = renderGroups(agent.blocks, openAll); + const title = escapeHtml(agent.title); + const openAttr = openAll ? ' open' : (idx === 0 && agentIdx === 0 ? ' open' : ''); + return `
${title}
${inner}
`; + }).join(''); + } + } const inner = blocks.length ? renderGroups(blocks, openAll) : `
${renderBasicMarkdown(section.lines.join('\n').trim())}
`;