import asyncio import io import json import re import threading from agent_server.sanitizing_think_tags import scrub_think_tags class QueueWriter(io.TextIOBase): """ File-like object that pushes each write to an asyncio.Queue immediately. """ def __init__(self, q: "asyncio.Queue"): self.q = q self._lock = threading.Lock() self._buf = [] # accumulate until newline to reduce spam def write(self, s: str): if not s: return 0 with self._lock: self._buf.append(s) # flush on newline to keep granularity reasonable if "\n" in s: chunk = "".join(self._buf) self._buf.clear() try: self.q.put_nowait({"__stdout__": chunk}) except Exception: pass return len(s) def flush(self): with self._lock: if self._buf: chunk = "".join(self._buf) self._buf.clear() try: self.q.put_nowait({"__stdout__": chunk}) except Exception: pass def _serialize_step(step) -> str: """ Best-effort pretty string for a smolagents MemoryStep / ActionStep. Works even if attributes are missing on some versions. """ parts = [] sn = getattr(step, "step_number", None) if sn is not None: parts.append(f"Step {sn}") thought_val = getattr(step, "thought", None) if thought_val: parts.append(f"Thought: {scrub_think_tags(str(thought_val))}") tool_val = getattr(step, "tool", None) if tool_val: parts.append(f"Tool: {scrub_think_tags(str(tool_val))}") code_val = getattr(step, "code", None) if code_val: code_str = scrub_think_tags(str(code_val)).strip() parts.append("```python\n" + code_str + "\n```") args = getattr(step, "args", None) if args: try: parts.append( "Args: " + scrub_think_tags(json.dumps(args, ensure_ascii=False)) ) except Exception: parts.append("Args: " + scrub_think_tags(str(args))) error = getattr(step, "error", None) if error: parts.append(f"Error: {scrub_think_tags(str(error))}") obs = getattr(step, "observations", None) if obs is not None: if isinstance(obs, (list, tuple)): obs_str = "\n".join(map(str, obs)) else: obs_str = str(obs) parts.append("Observation:\n" + scrub_think_tags(obs_str).strip()) # If this looks like a FinalAnswer step object, surface a clean final answer try: tname = type(step).__name__ except Exception: tname = "" if tname.lower().startswith("finalanswer"): out = getattr(step, "output", None) if out is not None: return f"Final answer: {scrub_think_tags(str(out)).strip()}" # Fallback: try to parse from string repr "FinalAnswerStep(output=...)" s = scrub_think_tags(str(step)) m = re.search(r"FinalAnswer[^()]*\(\s*output\s*=\s*([^,)]+)", s) if m: return f"Final answer: {m.group(1).strip()}" # If the only content would be an object repr like FinalAnswerStep(...), drop it; # a cleaner "Final answer: ..." will come from the rule above or stdout. joined = "\n".join(parts).strip() if re.match(r"^FinalAnswer[^\n]+\)$", joined): return "" return joined or scrub_think_tags(str(step))