""" StreamingCallback:拦截 agent 的输出,转成结构化 SSE 事件 后端通过 patch agent 的 print 输出方法捕获步骤事件,并转成 SSE。 """ import io import queue import re import sys class StreamingCallback: """ 拦截 agent.go() 的 stdout 输出,解析成结构化事件。 Biomni 的 agent 通过 print() 输出中间步骤: - "Thought: ..." → thinking 事件 - "```python\n...\n```" → code 事件 - "Observation: ..." → observation 事件 - 最终答案 → result 事件 """ def __init__(self, event_queue: queue.Queue): self.q = event_queue self._original_stdout = None self._buf = io.StringIO() self._in_solution = False self._solution_lines: list[str] = [] def attach(self, _agent): """patch sys.stdout 来捕获 agent 的 print 输出""" self._original_stdout = sys.stdout sys.stdout = _InterceptWriter(self._parse_and_emit, self._original_stdout) def detach(self, _agent): # 兜底:若流结束时仍在 solution 块内,仍然输出 result self._flush_solution_if_any() if self._original_stdout: sys.stdout = self._original_stdout def _parse_and_emit(self, text: str): """解析 agent 输出文本,转成结构化事件""" raw_text = text.rstrip("\n") if self._handle_solution_markup(raw_text): return text = raw_text.strip() if not text: return # 匹配 Biomni A1 的输出格式 if text.startswith("Thought:") or text.startswith("[Thinking]"): self.q.put({"type": "thinking", "content": text}) elif "```python" in text or "```r" in text or "```bash" in text: # 提取代码块 match = re.search(r"```(\w+)\n(.*?)```", text, re.DOTALL) if match: lang, code = match.group(1), match.group(2) self.q.put({"type": "code", "lang": lang, "content": code}) else: self.q.put({"type": "thinking", "content": text}) elif text.startswith("Observation:") or text.startswith("[Output]"): self.q.put({"type": "observation", "content": text}) elif text.startswith("Action:") or "tool" in text.lower()[:30]: # 尝试提取工具名 tool_match = re.search(r"(\w+)\(", text) tool_name = tool_match.group(1) if tool_match else "tool" self.q.put({"type": "tool_use", "tool": tool_name, "content": text}) elif text.startswith("Final Answer:") or text.startswith("Answer:"): self.q.put({"type": "result", "content": text}) else: # 其他输出作为 thinking self.q.put({"type": "thinking", "content": text}) def _handle_solution_markup(self, raw_text: str) -> bool: """ 解析 ... 多行块。 命中时输出 type=result,并返回 True 表示该行已被消费。 """ if self._in_solution: if "" in raw_text: before_end, _after_end = raw_text.split("", 1) if before_end: self._solution_lines.append(before_end) self._flush_solution_if_any() self._in_solution = False else: self._solution_lines.append(raw_text) return True if "" in raw_text: _before_start, after_start = raw_text.split("", 1) self._in_solution = True self._solution_lines = [] if "" in after_start: content, _after_end = after_start.split("", 1) if content: self._solution_lines.append(content) self._flush_solution_if_any() self._in_solution = False else: if after_start: self._solution_lines.append(after_start) return True return False def _flush_solution_if_any(self): content = "\n".join(self._solution_lines).strip() self._solution_lines = [] if content: self.q.put({"type": "result", "content": content}) class _InterceptWriter: """替换 sys.stdout,拦截写入""" def __init__(self, callback, original): self._cb = callback self._orig = original self._line_buf = "" def write(self, text): self._orig.write(text) # 同时写到真实 stdout(方便调试) self._line_buf += text if "\n" in self._line_buf: lines = self._line_buf.split("\n") for line in lines[:-1]: self._cb(line) self._line_buf = lines[-1] def flush(self): if self._line_buf: self._cb(self._line_buf) self._line_buf = "" self._orig.flush() def __getattr__(self, name): return getattr(self._orig, name)