refactor(tools): 输出处理原语移出 base.py——base 只留 Tool 基类与路径边界
compact_tool_output / format_timeout_result 是「工具输出处理」关注点,与 Tool ABC / FileOutOfBounds 路径安全混装在 base.py(审查小项)。析出 tools/output.py,8 处引用(5 工具 + executor_docker + 2 测试)同步改。 318 测试全过。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8038fb7491
commit
5a5ebfbe24
|
|
@ -47,13 +47,13 @@ import json
|
|||
# canceller 侧线程 poll cancel_check 的间隔;单测可 patch 此常量加速
|
||||
_CANCEL_POLL_INTERVAL_S = 0.2
|
||||
|
||||
from tools.base import format_timeout_result
|
||||
from tools.output import format_timeout_result
|
||||
|
||||
from .executor import ExecCtx, Executor, ToolResult
|
||||
from .executor_host import HostExecutor
|
||||
from .pysyntax import precheck_python
|
||||
from .sandbox import SandboxPool
|
||||
from tools.base import compact_tool_output
|
||||
from tools.output import compact_tool_output
|
||||
|
||||
|
||||
# write/edit 走配额 gate;read/glob/grep 不消耗磁盘,放行
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||
import tempfile
|
||||
import unittest
|
||||
|
||||
from tools.base import format_timeout_result
|
||||
from tools.output import format_timeout_result
|
||||
from tools.run_python import RunPythonTool
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import unittest
|
||||
import tempfile
|
||||
|
||||
from tools.base import compact_tool_output
|
||||
from tools.output import compact_tool_output
|
||||
from tools.run_python import RunPythonTool
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,52 +16,6 @@ class FileOutOfBounds(Exception):
|
|||
"""agent 给的文件路径解析后落在 user_root 之外(防 `../` 读到别人/系统文件)。"""
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class Tool(ABC):
|
||||
name: str = ""
|
||||
description: str = ""
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ from typing import Optional
|
|||
|
||||
from core import procs
|
||||
|
||||
from .base import Tool, compact_tool_output
|
||||
from .base import Tool
|
||||
from .output import compact_tool_output
|
||||
|
||||
|
||||
class CheckProcessTool(Tool):
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ from uuid import UUID
|
|||
from core.ark_client import ArkConfig
|
||||
from core.storage.usage import record_vision_usage
|
||||
|
||||
from .base import Tool, compact_tool_output
|
||||
from .base import Tool
|
||||
from .output import compact_tool_output
|
||||
from .image_ref import load_image_as_data_url
|
||||
from .media_common import ark_chat_with_retry, extract_chat_answer, record_usage_safe
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
"""工具输出处理原语(从 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)
|
||||
|
|
@ -18,7 +18,8 @@ from uuid import UUID
|
|||
from core.ark_client import ArkConfig
|
||||
from core.storage.usage import record_vision_usage
|
||||
|
||||
from .base import Tool, compact_tool_output
|
||||
from .base import Tool
|
||||
from .output import compact_tool_output
|
||||
from .image_ref import _CONTAINER_ROOT, load_pdf_as_data_url, resolve_in_root
|
||||
from .media_common import ark_chat_with_retry, extract_chat_answer, record_usage_safe
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ from typing import Optional
|
|||
from core import procs
|
||||
from core.pysyntax import precheck_python
|
||||
|
||||
from .base import Tool, compact_tool_output, format_timeout_result
|
||||
from .base import Tool
|
||||
from .output import compact_tool_output, format_timeout_result
|
||||
|
||||
_SENSITIVE_PATTERNS = ("API_KEY", "TOKEN", "SECRET", "PASSWORD", "PRIVATE_KEY")
|
||||
# 刻意放行的例外:research skill 在 sandbox 里直连 paper_server,这把只读文献库 key
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ from typing import Optional
|
|||
|
||||
from core import procs
|
||||
|
||||
from .base import Tool, compact_tool_output
|
||||
from .base import Tool
|
||||
from .output import compact_tool_output
|
||||
|
||||
|
||||
class ShellTool(Tool):
|
||||
|
|
|
|||
Loading…
Reference in New Issue