Compare commits

...

8 Commits

Author SHA1 Message Date
c8847b0dbb use annotated sandbox 2026-02-28 17:09:14 +08:00
c4ad6433cb add daytona implementation 2026-02-28 17:06:38 +08:00
bd4dfaad2a rename 2026-02-28 17:06:25 +08:00
c3d748c08f rename 2026-02-28 17:06:17 +08:00
8558e60ee6 import localshell in __init__.py 2026-02-28 16:52:24 +08:00
abb78ad70e localshell backend implementation 2026-02-28 16:52:10 +08:00
262d7dd51b moved things to base class 2026-02-28 16:29:24 +08:00
c64df2f48a ignore workspace 2026-02-28 16:05:21 +08:00
8 changed files with 150 additions and 16 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
.vscode/
logs/
*.egg-info/
workspace/
*.pyc
*.zip

View File

@@ -2,9 +2,13 @@ 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)

View File

@@ -7,6 +7,7 @@ from abc import ABC, abstractmethod
class BaseFilesystemBackend(ABC):
backend: Any
config: Any
@abstractmethod
def _build_backend(self):
@@ -21,4 +22,7 @@ class BaseFilesystemBackend(ABC):
def get_deepagent_params(self):
"""extra params to pass into the creation of deepagents"""
if hasattr(self.config, "rt_skills_dir"):
return {"skills" : [self.config.rt_skills_dir]}
else:
return {}

View File

@@ -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")

View File

@@ -0,0 +1,39 @@
from dataclasses import dataclass, field, is_dataclass
from typing import Type, TypedDict, Literal, Dict, List, Tuple, Optional
import tyro
import os.path as osp
from abc import ABC, abstractmethod
import glob
from loguru import logger
from deepagents.backends.utils import create_file_data
from deepagents.backends import LocalShellBackend
from lang_agent.config import InstantiateConfig
from lang_agent.fs_bkends import BaseFilesystemBackend
@tyro.conf.configure(tyro.conf.SuppressFixed)
@dataclass
class LocalShellConfig(InstantiateConfig):
_target:Type = field(default_factory=lambda:LocalShell)
workspace_dir:str = "./workspace"
"""path to workspace directory"""
skills_dir:str = "./workspace/skills"
"""path to directory containing skill files"""
rt_skills_dir:str = "/skills"
"""path to directory with skills in runtime directory"""
class LocalShell(BaseFilesystemBackend):
def __init__(self, config:LocalShellConfig):
self.config = config
self._build_backend()
def _build_backend(self):
self.backend = LocalShellBackend(root_dir=self.config.workspace_dir,
virtual_mode=True,
env={"PATH": "/usr/bin:/bin"})

View File

@@ -55,16 +55,6 @@ class StateBk(BaseFilesystemBackend):
self.skills_dict = build_skill_fs_dict(self.config.skills_dir)
self.backend = lambda rt : StateBackend(rt)
def get_backend(self):
return self.backend
def _get_rt_skill_dir(self)->List[str]:
"""get runtime skill dir"""
return [self.config.rt_skills_dir]
def get_inf_inp(self):
"""get inference input for deepagent"""
return {"files":self.skills_dict}
def get_deepagent_params(self):
return {"skills" : self._get_rt_skill_dir()}

View File

@@ -5,7 +5,7 @@ from lang_agent.graphs.routing import RoutingConfig, RoutingGraph
from lang_agent.graphs.dual_path import DualConfig, Dual
from lang_agent.graphs.vision_routing import VisionRoutingConfig, VisionRoutingGraph
# from lang_agent.graphs.child_demo import ChildDemoGraphConfig, ChildDemoGraph
from lang_agent.graphs.qt_deepagents import DeepAgentConfig
from lang_agent.graphs.deepagents_qt import DeepAgentConfig
graph_dict = {
"react": ReactGraphConfig(),

View File

@@ -12,12 +12,14 @@ from deepagents import create_deep_agent
from lang_agent.utils import make_llm
from lang_agent.components.tool_manager import ToolManager, ToolManagerConfig
from lang_agent.fs_bkends import StateBk, StateBkConfig
from lang_agent.components.prompt_store import build_prompt_store
from lang_agent.graphs.graph_states import State
from lang_agent.config import LLMNodeConfig
from lang_agent.base import GraphBase
# from lang_agent.fs_bkends import StateBk, StateBkConfig, LocalShell, LocalShellConfig, DaytonaSandboxBk, DaytonaSandboxConfig
from lang_agent.fs_bkends import BaseFilesystemBackend, StateBkConfig, AnnotatedStateBk
@tyro.conf.configure(tyro.conf.SuppressFixed)
@dataclass
class DeepAgentConfig(LLMNodeConfig):
@@ -28,7 +30,9 @@ class DeepAgentConfig(LLMNodeConfig):
tool_manager_config: ToolManagerConfig = field(default_factory=ToolManagerConfig)
file_backend_config: StateBkConfig = field(default_factory=StateBkConfig)
# file_backend_config: StateBkConfig = field(default_factory=StateBkConfig)
# file_backend_config: LocalShellConfig = field(default_factory=LocalShellConfig)
file_backend_config: AnnotatedStateBk = field(default_factory=StateBkConfig)
def __post_init__(self):
super().__post_init__()
@@ -47,7 +51,7 @@ class DeepAgent(GraphBase):
tags=["main_llm"])
self.tool_man: ToolManager = self.config.tool_manager_config.setup()
self.file_backend: StateBk = self.config.file_backend_config.setup()
self.file_backend: BaseFilesystemBackend = self.config.file_backend_config.setup()
bkend_agent_params = self.file_backend.get_deepagent_params()
self.mem = MemorySaver()