93 lines
3.3 KiB
Python
93 lines
3.3 KiB
Python
from dataclasses import dataclass, field
|
|
from typing import Type, Optional
|
|
from pathlib import Path
|
|
import os
|
|
import tyro
|
|
from loguru import logger
|
|
|
|
from daytona import Daytona, DaytonaConfig, FileUpload
|
|
from langchain_daytona import DaytonaSandbox
|
|
|
|
from lang_agent.fs_bkends.base import BaseFilesystemBackend, FilesystemBackendConfig
|
|
|
|
|
|
@tyro.conf.configure(tyro.conf.SuppressFixed)
|
|
@dataclass
|
|
class DaytonaSandboxConfig(FilesystemBackendConfig):
|
|
_target: Type = field(default_factory=lambda: DaytonaSandboxBk)
|
|
|
|
api_key: Optional[str] = None
|
|
"""Daytona API key. Falls back to DAYTONA_API_KEY env var."""
|
|
|
|
skills_dir: str = "./workspace/skills"
|
|
"""local path to directory containing skill files to upload"""
|
|
|
|
rt_skills_dir: str = ""
|
|
"""runtime skills path inside the sandbox (auto-set from sandbox workdir)"""
|
|
|
|
def __post_init__(self):
|
|
super().__post_init__()
|
|
if self.api_key is None:
|
|
self.api_key = os.environ.get("DAYTONA_API_KEY")
|
|
if self.api_key is None:
|
|
logger.error("no DAYTONA_API_KEY provided")
|
|
else:
|
|
logger.info("DAYTONA_API_KEY loaded from environ")
|
|
|
|
|
|
class DaytonaSandboxBk(BaseFilesystemBackend):
|
|
def __init__(self, config: DaytonaSandboxConfig):
|
|
self.config = config
|
|
self.sandbox = None
|
|
self._build_backend()
|
|
|
|
def _build_backend(self):
|
|
daytona = Daytona(DaytonaConfig(api_key=self.config.api_key))
|
|
self.sandbox = daytona.create()
|
|
workdir = self.sandbox.get_work_dir()
|
|
logger.info(f"Daytona sandbox created: {self.sandbox.id}, workdir: {workdir}")
|
|
|
|
if not self.config.rt_skills_dir:
|
|
self.config.rt_skills_dir = f"{workdir}/skills"
|
|
|
|
self._upload_skills(workdir)
|
|
self.backend = DaytonaSandbox(sandbox=self.sandbox)
|
|
|
|
def _upload_skills(self, workdir: str):
|
|
skills_dir = Path(self.config.skills_dir)
|
|
if not skills_dir.exists():
|
|
logger.warning(f"Skills directory not found: {skills_dir}")
|
|
return
|
|
|
|
files_to_upload = []
|
|
for skill_path in skills_dir.rglob("*"):
|
|
if not skill_path.is_file():
|
|
continue
|
|
relative_path = skill_path.relative_to(skills_dir)
|
|
remote_path = f"{workdir}/skills/{relative_path.as_posix()}"
|
|
with open(skill_path, "rb") as f:
|
|
files_to_upload.append(FileUpload(source=f.read(), destination=remote_path))
|
|
|
|
if not files_to_upload:
|
|
logger.warning("No skill files found to upload")
|
|
return
|
|
|
|
unique_dirs = {str(Path(u.destination).parent) for u in files_to_upload}
|
|
for dir_path in sorted(unique_dirs):
|
|
try:
|
|
self.sandbox.fs.create_folder(dir_path, "755")
|
|
except Exception as e:
|
|
if "permission denied" not in str(e).lower():
|
|
logger.debug(f"Creating dir {dir_path}: {e}")
|
|
|
|
self.sandbox.fs.upload_files(files_to_upload)
|
|
logger.info(f"Uploaded {len(files_to_upload)} skill files to {workdir}/skills/")
|
|
|
|
def get_deepagent_params(self):
|
|
return {"skills": [self.config.rt_skills_dir]}
|
|
|
|
def stop(self):
|
|
if self.sandbox is not None:
|
|
self.sandbox.stop()
|
|
logger.info("Daytona sandbox stopped")
|