Files
deepagent/deepagents_sourcecode/libs/deepagents-cli/deepagents_cli/integrations/runloop.py
HyunjunJeon 9cb01f4abe project init
2025-12-31 11:32:36 +09:00

122 lines
4.8 KiB
Python

"""Runloop을 위한 BackendProtocol 구현."""
try:
import runloop_api_client
except ImportError:
msg = (
"RunloopBackend를 위해서는 runloop_api_client 패키지가 필요합니다. "
"`pip install runloop_api_client`로 설치하십시오."
)
raise ImportError(msg)
import os
from deepagents.backends.protocol import ExecuteResponse, FileDownloadResponse, FileUploadResponse
from deepagents.backends.sandbox import BaseSandbox
from runloop_api_client import Runloop
class RunloopBackend(BaseSandbox):
"""Runloop devbox의 파일에서 작동하는 백엔드.
이 구현은 Runloop API 클라이언트를 사용하여 명령을 실행하고
원격 devbox 환경 내에서 파일을 조작합니다.
"""
def __init__(
self,
devbox_id: str,
client: Runloop | None = None,
api_key: str | None = None,
) -> None:
"""Runloop 프로토콜을 초기화합니다.
Args:
devbox_id: 작업할 Runloop devbox의 ID.
client: 선택적인 기존 Runloop 클라이언트 인스턴스
api_key: 새 클라이언트를 생성하기 위한 선택적 API 키
(기본값은 RUNLOOP_API_KEY 환경 변수)
"""
if client and api_key:
msg = "client 또는 bearer_token 중 하나만 제공해야 하며, 둘 다 제공할 수는 없습니다."
raise ValueError(msg)
if client is None:
api_key = api_key or os.environ.get("RUNLOOP_API_KEY", None)
if api_key is None:
msg = "client 또는 bearer_token 중 하나는 제공되어야 합니다."
raise ValueError(msg)
client = Runloop(bearer_token=api_key)
self._client = client
self._devbox_id = devbox_id
self._timeout = 30 * 60
@property
def id(self) -> str:
"""샌드박스 백엔드의 고유 식별자."""
return self._devbox_id
def execute(
self,
command: str,
) -> ExecuteResponse:
"""devbox에서 명령을 실행하고 ExecuteResponse를 반환합니다.
Args:
command: 실행할 전체 셸 명령 문자열.
Returns:
결합된 출력, 종료 코드, 선택적 시그널 및 잘림 플래그가 포함된 ExecuteResponse.
"""
result = self._client.devboxes.execute_and_await_completion(
devbox_id=self._devbox_id,
command=command,
timeout=self._timeout,
)
# stdout과 stderr 결합
output = result.stdout or ""
if result.stderr:
output += "\n" + result.stderr if output else result.stderr
return ExecuteResponse(
output=output,
exit_code=result.exit_status,
truncated=False, # Runloop는 잘림 정보를 제공하지 않음
)
def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
"""Runloop devbox에서 여러 파일을 다운로드합니다.
Runloop API를 사용하여 파일을 개별적으로 다운로드합니다. 순서를 유지하고
예외를 발생시키는 대신 파일별 오류를 보고하는 FileDownloadResponse 객체 목록을 반환합니다.
TODO: 표준화된 FileOperationError 코드를 사용하여 적절한 오류 처리를 구현해야 합니다.
현재는 정상적인 동작(happy path)만 구현되어 있습니다.
"""
responses: list[FileDownloadResponse] = []
for path in paths:
# devboxes.download_file은 .read()를 노출하는 BinaryAPIResponse를 반환함
resp = self._client.devboxes.download_file(self._devbox_id, path=path)
content = resp.read()
responses.append(FileDownloadResponse(path=path, content=content, error=None))
return responses
def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
"""Runloop devbox에 여러 파일을 업로드합니다.
Runloop API를 사용하여 파일을 개별적으로 업로드합니다. 순서를 유지하고
예외를 발생시키는 대신 파일별 오류를 보고하는 FileUploadResponse 객체 목록을 반환합니다.
TODO: 표준화된 FileOperationError 코드를 사용하여 적절한 오류 처리를 구현해야 합니다.
현재는 정상적인 동작(happy path)만 구현되어 있습니다.
"""
responses: list[FileUploadResponse] = []
for path, content in files:
# Runloop 클라이언트는 'file'을 바이트 또는 파일류 객체로 기대함
self._client.devboxes.upload_file(self._devbox_id, path=path, file=content)
responses.append(FileUploadResponse(path=path, error=None))
return responses