"""工具输出处理原语(从 tools/base.py 析出,2026-07-23——base 只留 Tool 基类与路径边界)。""" from __future__ import annotations def compact_tool_output( text: str, *, max_chars: int = 8_000, head_chars: int = 4_000, tail_chars: int = 2_000, ) -> str: """压缩长工具输出,保留头尾和截断说明。""" if len(text) <= max_chars: return text head_chars = max(0, min(head_chars, max_chars)) tail_chars = max(0, min(tail_chars, max_chars - head_chars)) removed = len(text) - head_chars - tail_chars return ( text[:head_chars] + f"\n[... truncated, {removed} chars omitted ...]\n" + (text[-tail_chars:] if tail_chars else "") ) _TIMEOUT_HINT = ( "若是批量循环(检索 / PDF 抽取 / 下载等):① 据上面已完成的部分**续跑剩下的**,别整批重跑;" "② 每完成一项就落盘 / 打印进度,超时也不丢;③ 大批量 / 大下载改用 background=true 后台跑," "再用 check_process 取结果;④ 单次别塞太多项。" ) def format_timeout_result(stdout: str, stderr: str, timeout_s: int) -> str: """run_python / shell 超时结果:带上超时前已捕获的部分输出 + 续跑提示。 进程被 kill 前跑出的 stdout/stderr 本已捕获(subprocess 标准行为:TimeoutExpired 带 .stdout/.stderr、docker 路径 kill 后 communicate() 续读),旧实现直接丢弃只回一句 超时 → 模型看不到「跑到哪了」、整批重来(工具失败面板 #8:批量检索 / PDF 抽取 / 下载 超时反复重跑,累计最多)。这里把部分输出一并返回,让模型据此续跑未完成的。""" parts = [] if stdout and stdout.strip(): parts.append(f"[stdout]\n{stdout.rstrip()}") if stderr and stderr.strip(): parts.append(f"[stderr]\n{stderr.rstrip()}") parts.append( f"[Error] command timed out after {timeout_s}s(进程已被杀,上方为超时前的部分输出)。" ) parts.append(_TIMEOUT_HINT) return "\n".join(parts)