Files
2026-01-11 14:05:05 +09:00

265 lines
9.4 KiB
Python

"""Docker 공유 작업공간 백엔드.
여러 SubAgent가 동일한 Docker 컨테이너 작업공간을 공유하는 백엔드입니다.
## 설계 배경
DeepAgents의 기본 구조에서 SubAgent들은 독립된 컨텍스트를 가지지만,
파일시스템을 공유해야 하는 경우가 있습니다:
- 연구 결과물 공유
- 중간 생성물 활용
- 협업 워크플로우
## 아키텍처
```
┌─────────────────────────────────────────────────────────────┐
│ Main Agent (Orchestrator) │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │SubAgent A │ │SubAgent B │ │SubAgent C │ │
│ │(Research) │ │(Analysis) │ │(Synthesis)│ │
│ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
│ │ │ │ │
│ └──────────────┼──────────────┘ │
│ ▼ │
│ SharedDockerBackend │
│ │ │
└───────────────────────┼─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Docker Container │
│ ┌─────────────────────────────────────────────────────────│
│ │ /workspace (공유 작업 디렉토리) │
│ │ ├── research/ (SubAgent A 출력) │
│ │ ├── analysis/ (SubAgent B 출력) │
│ │ └── synthesis/ (SubAgent C 출력) │
│ └─────────────────────────────────────────────────────────│
└─────────────────────────────────────────────────────────────┘
```
## 보안 고려사항
1. **컨테이너 격리**: 호스트 시스템과 격리
2. **볼륨 마운트**: 필요한 디렉토리만 마운트
3. **네트워크 정책**: 필요시 네트워크 격리
4. **리소스 제한**: CPU/메모리 제한 설정
"""
from dataclasses import dataclass
from typing import Any
@dataclass
class DockerConfig:
image: str = "python:3.11-slim"
workspace_path: str = "/workspace"
memory_limit: str = "2g"
cpu_limit: float = 2.0
network_mode: str = "none"
auto_remove: bool = True
timeout_seconds: int = 300
@dataclass
class ExecuteResponse:
output: str
exit_code: int | None = None
truncated: bool = False
error: str | None = None
@dataclass
class WriteResult:
path: str
error: str | None = None
files_update: dict[str, Any] | None = None
@dataclass
class EditResult:
path: str
occurrences: int = 0
error: str | None = None
files_update: dict[str, Any] | None = None
class SharedDockerBackend:
"""여러 SubAgent가 공유하는 Docker 작업공간 백엔드.
Args:
config: Docker 설정
container_id: 기존 컨테이너 ID (재사용 시)
"""
def __init__(
self,
config: DockerConfig | None = None,
container_id: str | None = None,
) -> None:
self.config = config or DockerConfig()
self._container_id = container_id
self._docker_client: Any = None
def _get_docker_client(self) -> Any:
if self._docker_client is None:
try:
import docker
self._docker_client = docker.from_env()
except ImportError:
raise RuntimeError(
"docker 패키지가 설치되지 않았습니다: pip install docker"
)
return self._docker_client
def _ensure_container(self) -> str:
if self._container_id:
return self._container_id
client = self._get_docker_client()
container = client.containers.run(
self.config.image,
command="tail -f /dev/null",
detach=True,
mem_limit=self.config.memory_limit,
nano_cpus=int(self.config.cpu_limit * 1e9),
network_mode=self.config.network_mode,
auto_remove=self.config.auto_remove,
)
self._container_id = container.id
return container.id
def execute(self, command: str) -> ExecuteResponse:
try:
container_id = self._ensure_container()
client = self._get_docker_client()
container = client.containers.get(container_id)
exec_result = container.exec_run(
command,
workdir=self.config.workspace_path,
)
output = exec_result.output.decode("utf-8", errors="replace")
truncated = len(output) > 100000
if truncated:
output = output[:100000] + "\n[출력이 잘렸습니다...]"
return ExecuteResponse(
output=output,
exit_code=exec_result.exit_code,
truncated=truncated,
)
except Exception as e:
return ExecuteResponse(
output="",
exit_code=1,
error=str(e),
)
async def aexecute(self, command: str) -> ExecuteResponse:
return self.execute(command)
def read(self, path: str, offset: int = 0, limit: int = 500) -> str:
full_path = f"{self.config.workspace_path}{path}"
result = self.execute(f"sed -n '{offset + 1},{offset + limit}p' {full_path}")
if result.error:
return f"파일 읽기 오류: {result.error}"
return result.output
async def aread(self, path: str, offset: int = 0, limit: int = 500) -> str:
return self.read(path, offset, limit)
def write(self, path: str, content: str) -> WriteResult:
full_path = f"{self.config.workspace_path}{path}"
dir_path = "/".join(full_path.split("/")[:-1])
self.execute(f"mkdir -p {dir_path}")
escaped_content = content.replace("'", "'\\''")
result = self.execute(f"echo '{escaped_content}' > {full_path}")
if result.exit_code != 0:
return WriteResult(path=path, error=result.output or result.error)
return WriteResult(path=path)
async def awrite(self, path: str, content: str) -> WriteResult:
return self.write(path, content)
def ls_info(self, path: str) -> list[dict[str, Any]]:
full_path = f"{self.config.workspace_path}{path}"
result = self.execute(f"ls -la {full_path}")
if result.error:
return []
files = []
for line in result.output.strip().split("\n")[1:]:
parts = line.split()
if len(parts) >= 9:
name = " ".join(parts[8:])
files.append(
{
"path": f"{path}/{name}".replace("//", "/"),
"is_dir": line.startswith("d"),
}
)
return files
async def als_info(self, path: str) -> list[dict[str, Any]]:
return self.ls_info(path)
def cleanup(self) -> None:
if self._container_id and self._docker_client:
try:
container = self._docker_client.containers.get(self._container_id)
container.stop()
except Exception:
pass
self._container_id = None
def __enter__(self) -> "SharedDockerBackend":
return self
def __exit__(self, *args: Any) -> None:
self.cleanup()
SHARED_WORKSPACE_DESIGN_DOC = """
# 공유 작업공간 설계
## 문제점
DeepAgents의 SubAgent들은 독립된 컨텍스트를 가지지만,
연구 워크플로우에서는 파일시스템 공유가 필요한 경우가 많습니다.
예시:
1. Research Agent가 수집한 데이터를 Analysis Agent가 처리
2. 여러 Agent가 생성한 결과물을 Synthesis Agent가 통합
3. 중간 체크포인트를 다른 Agent가 이어서 작업
## 해결책: SharedDockerBackend
1. **단일 컨테이너**: 모든 SubAgent가 동일한 Docker 컨테이너 사용
2. **격리된 디렉토리**: 각 SubAgent는 자신의 디렉토리에서 작업
3. **공유 영역**: /workspace/shared 같은 공유 디렉토리 운영
## 장점
- 파일 복사 오버헤드 없음
- 실시간 결과 공유
- 일관된 실행 환경
## 단점
- 컨테이너 수명 관리 필요
- 동시 접근 충돌 가능성
- 보안 경계가 SubAgent 간에는 없음
"""