diff --git a/lang_agent/fs_bkends/__init__.py b/lang_agent/fs_bkends/__init__.py index 302e0bc..a9bf1be 100644 --- a/lang_agent/fs_bkends/__init__.py +++ b/lang_agent/fs_bkends/__init__.py @@ -3,9 +3,12 @@ import tyro from lang_agent.fs_bkends.base import BaseFilesystemBackend from lang_agent.fs_bkends.statebk import StateBk, StateBkConfig from lang_agent.fs_bkends.localshell import LocalShell, LocalShellConfig +from lang_agent.fs_bkends.daytona_sandbox import DaytonaSandboxBk, DaytonaSandboxConfig statebk_dict = { "statebk": StateBkConfig(), + "localshell": LocalShellConfig(), + "daytonasandbox": DaytonaSandboxConfig(), } statebk_union = tyro.extras.subcommand_type_from_defaults(statebk_dict, prefix_names=False) diff --git a/lang_agent/fs_bkends/daytona_sandbox.py b/lang_agent/fs_bkends/daytona_sandbox.py new file mode 100644 index 0000000..e1ae07d --- /dev/null +++ b/lang_agent/fs_bkends/daytona_sandbox.py @@ -0,0 +1,92 @@ +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.config import InstantiateConfig +from lang_agent.fs_bkends import BaseFilesystemBackend + + +@tyro.conf.configure(tyro.conf.SuppressFixed) +@dataclass +class DaytonaSandboxConfig(InstantiateConfig): + _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): + 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")