agentscope-chat / src /agentscope /workspace /_docker /_docker_backend.py
Akshay66777's picture
AgentScope Gradio chat app β€” ZeroGPU-ready
9792ea7 verified
Raw
History Blame Contribute Delete
6.71 kB
# -*- coding: utf-8 -*-
"""Docker container :class:`BackendBase` implementation.
Wraps the ``aiodocker`` container APIs (``exec``, ``get_archive``,
``put_archive``) into the three backend primitives (``exec_shell``,
``read_file``, ``write_file``) so that builtin tools (Bash, Read,
Write, Edit, Grep, Glob) can operate inside a Docker container
transparently. All derived filesystem helpers (``file_exists``,
``is_dir``, ``list_dir``, ``stat_mtime``, ``delete_path``) are
inherited from :class:`BackendBase`, which implements them via
``exec_shell``.
"""
from __future__ import annotations
import asyncio
import io
import posixpath
import tarfile
from typing import Any
from ...tool import BackendBase, ExecResult
class DockerBackend(BackendBase):
"""Backend that delegates to a running Docker container.
Only the three abstract primitives (``exec_shell``, ``read_file``,
``write_file``) are implemented here; the derived filesystem helpers
are inherited from :class:`BackendBase`.
Args:
container (`Any`):
An ``aiodocker`` container object (must already be started).
workdir (`str`):
Default working directory for ``exec_shell`` calls inside
the container.
"""
def __init__(self, container: Any, workdir: str) -> None:
"""Initialize the Docker backend.
Args:
container (`Any`):
A started ``aiodocker`` container object.
workdir (`str`):
Default working directory for ``exec_shell`` calls
inside the container.
"""
self._container = container
self._workdir = workdir
# ── exec ───────────────────────────────────────────────────────
async def getcwd(self) -> str:
"""Return the container's default working directory.
Overrides the base class default (which would shell out to
``pwd``) with the cached ``workdir`` supplied at construction,
avoiding a per-call container ``exec`` round-trip.
Returns:
`str`:
The container's default working directory.
"""
return self._workdir
async def exec_shell(
self,
command: list[str],
*,
cwd: str | None = None,
timeout: float | None = None,
) -> ExecResult:
"""Run a program directly inside the container.
*command* is an argv list executed via the container ``exec``
API without an intervening shell. Callers needing shell features
wrap their command line as ``["sh", "-c", line]``.
Args:
command (`list[str]`):
Executable path/name followed by its arguments.
cwd (`str | None`, optional):
Working directory inside the container. When ``None``
the backend's default ``workdir`` is used.
timeout (`float | None`, optional):
Maximum number of seconds to wait before returning an
``exit_code`` of ``-1``. When ``None`` the call waits
indefinitely.
Returns:
`ExecResult`:
The captured exit code, stdout, and stderr.
"""
async def _run() -> ExecResult:
exec_obj = await self._container.exec(
cmd=command,
workdir=cwd or self._workdir,
)
stdout_parts: list[bytes] = []
stderr_parts: list[bytes] = []
async with exec_obj.start() as stream:
while True:
msg = await stream.read_out()
if msg is None:
break
if msg.stream == 1:
stdout_parts.append(msg.data)
else:
stderr_parts.append(msg.data)
inspect = await exec_obj.inspect()
code = inspect.get("ExitCode", -1)
if code is None:
code = -1
return ExecResult(
exit_code=int(code),
stdout=b"".join(stdout_parts),
stderr=b"".join(stderr_parts),
)
if timeout is None:
return await _run()
try:
return await asyncio.wait_for(_run(), timeout=timeout)
except asyncio.TimeoutError:
return ExecResult(
exit_code=-1,
stdout=b"",
stderr=b"timed out",
)
# ── file I/O ───────────────────────────────────────────────────
async def read_file(self, path: str) -> bytes:
"""Fetch a file from the container via ``get_archive``.
Args:
path (`str`):
Path to the file inside the container.
Returns:
`bytes`:
The raw file contents.
Raises:
`FileNotFoundError`:
If the path does not exist inside the container.
"""
from aiodocker import exceptions as aiodocker_exceptions
try:
tar = await self._container.get_archive(path)
except aiodocker_exceptions.DockerError as exc:
if exc.status == 404:
raise FileNotFoundError(
f"not found in container: {path}",
) from exc
raise
try:
for member in tar.getmembers():
if member.isfile():
f = tar.extractfile(member)
if f:
return f.read()
finally:
tar.close()
raise FileNotFoundError(f"not found in container: {path}")
async def write_file(self, path: str, data: bytes) -> None:
"""Write raw bytes to a file inside the container.
Creates the parent directory first since ``put_archive``
requires it to exist.
Args:
path (`str`):
Destination path inside the container.
data (`bytes`):
The raw bytes to write.
"""
parent = posixpath.dirname(path) or "/"
name = posixpath.basename(path)
await self.exec_shell(["mkdir", "-p", parent])
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w") as tf:
info = tarfile.TarInfo(name=name)
info.size = len(data)
tf.addfile(info, io.BytesIO(data))
await self._container.put_archive(parent, buf.getvalue())