86 lines
2.9 KiB
Python
86 lines
2.9 KiB
Python
"""read_file.py – tool for reading files inside the sandbox."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import TYPE_CHECKING
|
||
|
||
from langchain_core.tools import BaseTool, tool
|
||
from loguru import logger
|
||
|
||
from docker_agent_sandbox.tools._utils import _MAX_OUTPUT_CHARS
|
||
|
||
if TYPE_CHECKING:
|
||
from docker_agent_sandbox.sandbox import DockerSandbox
|
||
|
||
|
||
def make_read_file_tool(sandbox: "DockerSandbox") -> BaseTool:
|
||
"""Return a read_file tool bound to *sandbox*."""
|
||
|
||
@tool
|
||
def read_file(path: str, start_line: int = 1, end_line: int | None = None) -> str:
|
||
"""
|
||
Read a file at *path*, returning its contents as text.
|
||
|
||
*path* can be absolute (/tmp/re-agent/result.csv) or relative to the
|
||
working directory.
|
||
|
||
*start_line* is the 1-based line number to start reading from (default: 1).
|
||
|
||
*end_line* is the last line to include, inclusive (default: read as many
|
||
lines as the MAX_CHARS cap allows). Use start_line/end_line to page
|
||
through large files in chunks.
|
||
|
||
At most 5,000 characters are returned per call. If the requested range
|
||
exceeds this, output is truncated and a summary line is appended showing
|
||
how many lines were omitted.
|
||
|
||
Returns the (possibly truncated) file contents, or an error message.
|
||
"""
|
||
logger.debug(
|
||
"Reading file inside sandbox: {} start_line={} end_line={}",
|
||
path,
|
||
start_line,
|
||
end_line,
|
||
)
|
||
try:
|
||
data = sandbox.read_file(path)
|
||
except (FileNotFoundError, IsADirectoryError, RuntimeError) as exc:
|
||
return f"[ERROR reading {path!r}] {exc}"
|
||
|
||
lines = data.decode("utf-8", errors="replace").splitlines(keepends=True)
|
||
total_lines = len(lines)
|
||
|
||
# Clamp to valid range (1-based, inclusive)
|
||
start_idx = max(0, start_line - 1)
|
||
end_idx = total_lines if end_line is None else min(end_line, total_lines)
|
||
selected = lines[start_idx:end_idx]
|
||
|
||
# Enforce character cap
|
||
text = ""
|
||
last_included_line = start_idx # track how far we got
|
||
for i, line in enumerate(selected):
|
||
if len(text) + len(line) > _MAX_OUTPUT_CHARS:
|
||
break
|
||
text += line
|
||
last_included_line = start_idx + i + 1 # 1-based
|
||
|
||
# Build informative suffix
|
||
suffix_parts = []
|
||
if last_included_line < end_idx:
|
||
omitted = end_idx - last_included_line
|
||
suffix_parts.append(
|
||
f"[... {omitted} more lines not shown (char cap reached). "
|
||
f"Call again with start_line={last_included_line + 1}.]"
|
||
)
|
||
elif end_idx < total_lines:
|
||
suffix_parts.append(
|
||
f"[Showing lines {start_line}–{end_idx} of {total_lines} total.]"
|
||
)
|
||
|
||
if suffix_parts:
|
||
text += "\n" + " ".join(suffix_parts)
|
||
|
||
return text
|
||
|
||
return read_file
|