feat: Use docker api to pull files from container
This commit is contained in:
@@ -172,6 +172,42 @@ class DockerSandbox:
|
||||
# File I/O
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _resolve_path(self, path: str) -> Path:
|
||||
"""Resolve *path* against working_dir if relative."""
|
||||
p = Path(path)
|
||||
if not p.is_absolute() and self._working_dir is not None:
|
||||
p = Path(self._working_dir) / p
|
||||
return p
|
||||
|
||||
def read_file(self, path: str) -> bytes:
|
||||
"""
|
||||
Read the file at *path* from the container using ``get_archive``.
|
||||
|
||||
Returns the raw file bytes. Raises ``FileNotFoundError`` if the path
|
||||
does not exist and ``RuntimeError`` if the container is not running.
|
||||
"""
|
||||
if self._container is None:
|
||||
raise RuntimeError("Sandbox container is not running.")
|
||||
|
||||
p = self._resolve_path(path)
|
||||
|
||||
try:
|
||||
stream, _ = self._container.get_archive(str(p))
|
||||
except docker.errors.NotFound:
|
||||
raise FileNotFoundError(f"No such file in container: {path!r}") from None
|
||||
|
||||
buf = io.BytesIO()
|
||||
for chunk in stream:
|
||||
buf.write(chunk)
|
||||
buf.seek(0)
|
||||
|
||||
with tarfile.open(fileobj=buf) as tar:
|
||||
member = tar.getmembers()[0]
|
||||
f = tar.extractfile(member)
|
||||
if f is None:
|
||||
raise IsADirectoryError(f"{path!r} is a directory, not a file")
|
||||
return f.read()
|
||||
|
||||
def write_file(self, path: str, content: str) -> None:
|
||||
"""
|
||||
Write *content* to *path* inside the container using ``put_archive``.
|
||||
@@ -182,9 +218,7 @@ class DockerSandbox:
|
||||
if self._container is None:
|
||||
raise RuntimeError("Sandbox container is not running.")
|
||||
|
||||
p = Path(path)
|
||||
if not p.is_absolute() and self._working_dir is not None:
|
||||
p = Path(self._working_dir) / p
|
||||
p = self._resolve_path(path)
|
||||
encoded = content.encode("utf-8")
|
||||
|
||||
self._container.exec_run(["mkdir", "-p", str(p.parent)])
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from shlex import quote
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from langchain_core.tools import BaseTool, tool
|
||||
@@ -36,11 +35,21 @@ def make_edit_file_tool(sandbox: "DockerSandbox") -> BaseTool:
|
||||
|
||||
Returns a confirmation with the number of lines affected, or an error.
|
||||
"""
|
||||
logger.debug("Editing file inside sandbox: {!r}", path)
|
||||
exit_code, content = sandbox.exec(f"cat -- {quote(path)}")
|
||||
if exit_code != 0:
|
||||
return f"[ERROR reading {path!r} for edit] {content.strip()}"
|
||||
_MAX_EDIT_BYTES = 1_000_000 # 1 MB
|
||||
|
||||
logger.debug("Editing file inside sandbox: {!r}", path)
|
||||
try:
|
||||
data = sandbox.read_file(path)
|
||||
except (FileNotFoundError, IsADirectoryError, RuntimeError) as exc:
|
||||
return f"[ERROR reading {path!r} for edit] {exc}"
|
||||
|
||||
if len(data) > _MAX_EDIT_BYTES:
|
||||
return (
|
||||
f"[ERROR] {path!r} is {len(data)} bytes; edit_file only supports files "
|
||||
f"up to {_MAX_EDIT_BYTES} bytes."
|
||||
)
|
||||
|
||||
content = data.decode("utf-8", errors="replace")
|
||||
count = content.count(old_str)
|
||||
if count == 0:
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from shlex import quote
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from langchain_core.tools import BaseTool, tool
|
||||
@@ -25,36 +24,29 @@ def make_read_file_tool(sandbox: "DockerSandbox") -> BaseTool:
|
||||
|
||||
*offset* is the number of bytes to skip from the start of the file.
|
||||
*length* is the maximum number of bytes to return. If the file is
|
||||
longer than ``offset + length``, the
|
||||
output is trimmed and a summary line is appended showing how many
|
||||
bytes were omitted.
|
||||
longer than ``offset + length``, the output is trimmed and a summary
|
||||
line is appended showing how many bytes were omitted.
|
||||
|
||||
Returns the (possibly trimmed) file contents as text, or an error message.
|
||||
"""
|
||||
logger.debug(
|
||||
"Reading file inside sandbox: {} offset={} length={}", path, offset, length
|
||||
)
|
||||
exit_code, wc_out = sandbox.exec(f"wc -c -- {quote(path)}")
|
||||
if exit_code != 0:
|
||||
return f"[ERROR reading {path!r}] {wc_out.strip()}"
|
||||
try:
|
||||
total = int(wc_out.split()[0])
|
||||
except (ValueError, IndexError):
|
||||
return f"[ERROR parsing file size for {path!r}] {wc_out.strip()}"
|
||||
data = sandbox.read_file(path)
|
||||
except (FileNotFoundError, IsADirectoryError, RuntimeError) as exc:
|
||||
return f"[ERROR reading {path!r}] {exc}"
|
||||
|
||||
exit_code, chunk = sandbox.exec(
|
||||
f"dd if={quote(path)} iflag=skip_bytes,count_bytes"
|
||||
f" skip={offset} count={length} 2>/dev/null"
|
||||
)
|
||||
if exit_code != 0:
|
||||
return f"[ERROR reading {path!r}] {chunk.strip()}"
|
||||
total = len(data)
|
||||
chunk = data[offset : offset + length]
|
||||
text = chunk.decode("utf-8", errors="replace")
|
||||
|
||||
suffix = ""
|
||||
if offset + length < total:
|
||||
remaining = total - (offset + length)
|
||||
suffix = f"\n[... {remaining} more bytes not shown (total {total} bytes). Use offset/length to read further.]"
|
||||
elif offset > 0 or total > length:
|
||||
suffix = f"\n[File total: {total} bytes, showing {len(chunk)} chars from offset {offset}.]"
|
||||
return chunk + suffix
|
||||
suffix = f"\n[File total: {total} bytes, showing {len(chunk)} bytes from offset {offset}.]"
|
||||
return text + suffix
|
||||
|
||||
return read_file
|
||||
|
||||
Reference in New Issue
Block a user