ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from aworld.sandbox.base import Sandbox
|
||||
from aworld.sandbox.common import BaseSandbox
|
||||
from aworld.sandbox.models import SandboxEnvType
|
||||
from aworld.sandbox.implementations import LocalSandbox, KubernetesSandbox, SuperSandbox
|
||||
|
||||
|
||||
# For backward compatibility, use LocalSandbox as the default Sandbox implementation
|
||||
DefaultSandbox = LocalSandbox
|
||||
|
||||
|
||||
# Override Sandbox class constructor to create the appropriate sandbox based on env_type
|
||||
def create_sandbox(
|
||||
env_type: Optional[int] = None,
|
||||
sandbox_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
timeout: Optional[int] = None,
|
||||
mcp_servers: Optional[List[str]] = None,
|
||||
mcp_config: Optional[Any] = None,
|
||||
**kwargs
|
||||
) -> Sandbox:
|
||||
"""
|
||||
Factory function to create a sandbox instance based on the environment type.
|
||||
|
||||
Args:
|
||||
env_type: The environment type. Defaults to LOCAL if None.
|
||||
sandbox_id: Unique identifier for the sandbox. If None, one will be generated.
|
||||
metadata: Additional metadata for the sandbox.
|
||||
timeout: Timeout for sandbox operations.
|
||||
mcp_servers: List of MCP servers to use.
|
||||
mcp_config: Configuration for MCP servers.
|
||||
**kwargs: Additional parameters for specific sandbox types.
|
||||
|
||||
Returns:
|
||||
Sandbox: An instance of a sandbox implementation.
|
||||
|
||||
Raises:
|
||||
ValueError: If an invalid environment type is provided.
|
||||
"""
|
||||
env_type = env_type or SandboxEnvType.LOCAL
|
||||
|
||||
if env_type == SandboxEnvType.LOCAL:
|
||||
return LocalSandbox(
|
||||
sandbox_id=sandbox_id,
|
||||
metadata=metadata,
|
||||
timeout=timeout,
|
||||
mcp_servers=mcp_servers,
|
||||
mcp_config=mcp_config,
|
||||
**kwargs
|
||||
)
|
||||
elif env_type == SandboxEnvType.K8S:
|
||||
return KubernetesSandbox(
|
||||
sandbox_id=sandbox_id,
|
||||
metadata=metadata,
|
||||
timeout=timeout,
|
||||
mcp_servers=mcp_servers,
|
||||
mcp_config=mcp_config,
|
||||
**kwargs
|
||||
)
|
||||
elif env_type == SandboxEnvType.SUPERCOMPUTER:
|
||||
return SuperSandbox(
|
||||
sandbox_id=sandbox_id,
|
||||
metadata=metadata,
|
||||
timeout=timeout,
|
||||
mcp_servers=mcp_servers,
|
||||
mcp_config=mcp_config,
|
||||
**kwargs
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Invalid environment type: {env_type}")
|
||||
|
||||
|
||||
# Monkey patch the Sandbox class to make direct instantiation work
|
||||
old_init = Sandbox.__init__
|
||||
|
||||
def _sandbox_init(self, *args, **kwargs):
|
||||
if type(self) is Sandbox:
|
||||
# This should never be called directly, as __new__ will return a different type
|
||||
pass
|
||||
else:
|
||||
# Pass through to the original __init__ for actual implementations
|
||||
old_init(self, *args, **kwargs)
|
||||
|
||||
# Store the original __new__ method
|
||||
original_new = object.__new__
|
||||
|
||||
# Create a new __new__ method that intercepts Sandbox instantiation
|
||||
def _sandbox_new(cls, *args, **kwargs):
|
||||
if cls is Sandbox:
|
||||
# If trying to instantiate Sandbox directly, use our factory instead
|
||||
return create_sandbox(**kwargs)
|
||||
else:
|
||||
# For subclasses, use the original __new__
|
||||
return original_new(cls)
|
||||
|
||||
# Apply the monkey patches
|
||||
Sandbox.__init__ = _sandbox_init
|
||||
Sandbox.__new__ = _sandbox_new
|
||||
|
||||
|
||||
# Expose key classes and functions
|
||||
__all__ = [
|
||||
'Sandbox',
|
||||
'BaseSandbox',
|
||||
'LocalSandbox',
|
||||
'KubernetesSandbox',
|
||||
'SuperSandbox',
|
||||
'DefaultSandbox',
|
||||
'SandboxEnvType',
|
||||
'create_sandbox'
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
from abc import ABC
|
||||
|
||||
|
||||
class SandboxApiBase(ABC):
|
||||
|
||||
@staticmethod
|
||||
def _get_sandbox_id(sandbox_id: str) -> str:
|
||||
return f"{sandbox_id}"
|
||||
@@ -0,0 +1,79 @@
|
||||
import abc
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from aworld.sandbox.models import SandboxStatus, SandboxEnvType, SandboxInfo, SandboxCreateResponse, EnvConfig
|
||||
from aworld.sandbox.api.apibase import SandboxApiBase
|
||||
|
||||
|
||||
class BaseSandboxApi(SandboxApiBase, abc.ABC):
|
||||
"""
|
||||
Base class for sandbox API implementations.
|
||||
Defines the interface for interacting with different types of sandboxes.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def _create_sandbox(
|
||||
cls,
|
||||
env_type: int,
|
||||
env_config: EnvConfig,
|
||||
mcp_servers: Optional[List[str]] = None,
|
||||
mcp_config: Optional[Any] = None,
|
||||
) -> SandboxCreateResponse:
|
||||
"""
|
||||
Create a sandbox based on the environment type and configuration.
|
||||
|
||||
Args:
|
||||
env_type: The environment type (LOCAL, K8S, SUPERCOMPUTER).
|
||||
env_config: Environment configuration.
|
||||
mcp_servers: List of MCP servers to use.
|
||||
mcp_config: Configuration for MCP servers.
|
||||
|
||||
Returns:
|
||||
SandboxCreateResponse: Response containing sandbox information.
|
||||
"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def _get_mcp_configs(
|
||||
cls,
|
||||
mcp_servers: Optional[List[str]] = None,
|
||||
mcp_config: Optional[Any] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
env_type: Optional[int] = None,
|
||||
) -> Any:
|
||||
"""
|
||||
Get MCP configurations for the sandbox.
|
||||
|
||||
Args:
|
||||
mcp_servers: List of MCP servers to use.
|
||||
mcp_config: Configuration for MCP servers.
|
||||
metadata: Additional metadata for the sandbox.
|
||||
env_type: The environment type.
|
||||
|
||||
Returns:
|
||||
Any: Updated MCP configuration.
|
||||
"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
async def _remove_sandbox(
|
||||
cls,
|
||||
sandbox_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
env_type: Optional[int] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Remove the sandbox and clean up resources.
|
||||
|
||||
Args:
|
||||
sandbox_id: Unique identifier for the sandbox.
|
||||
metadata: Metadata for the sandbox.
|
||||
env_type: The environment type.
|
||||
|
||||
Returns:
|
||||
bool: True if removal was successful, False otherwise.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,218 @@
|
||||
import logging
|
||||
import time
|
||||
import datetime
|
||||
import random
|
||||
import string
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from aworld.sandbox.api.base_sandbox_api import BaseSandboxApi
|
||||
from aworld.sandbox.env_client.kubernetes.client import KubernetesApiClient
|
||||
from aworld.sandbox.models import SandboxStatus, SandboxEnvType, SandboxK8sResponse
|
||||
from aworld.sandbox.run.mcp_servers import McpServers
|
||||
|
||||
|
||||
class KubernetesSandboxApi(BaseSandboxApi):
|
||||
"""
|
||||
API implementation for Kubernetes sandbox operations.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _create_sandbox(
|
||||
cls,
|
||||
env_type: int,
|
||||
env_config: Any,
|
||||
mcp_servers: Optional[List[str]] = None,
|
||||
mcp_config: Optional[Any] = None,
|
||||
) -> SandboxK8sResponse:
|
||||
"""
|
||||
Create a Kubernetes sandbox based on the reference implementation.
|
||||
"""
|
||||
# Initialize these variables outside the try block to avoid accessing undefined variables in exception handling
|
||||
client = None
|
||||
pod_name = None
|
||||
service_name = None
|
||||
|
||||
try:
|
||||
# Generate current date and time as prefix, format is yymmddHHMMSS
|
||||
date_prefix = datetime.datetime.now().strftime("%y%m%d%H%M%S")
|
||||
random_str = cls.generate_random_string()
|
||||
pod_name = f"pod-{date_prefix}-{random_str}"
|
||||
service_name = f"service-{date_prefix}-{random_str}"
|
||||
logging.info(f"Generated pod_name: {pod_name}")
|
||||
logging.info(f"Generated service_name: {service_name}")
|
||||
|
||||
client = KubernetesApiClient()
|
||||
|
||||
pod_result = client.create_pod_from_yaml(pod_name=pod_name)
|
||||
if not pod_result:
|
||||
return None
|
||||
|
||||
max_attempts = 30
|
||||
attempts = 0
|
||||
wait_seconds = 2
|
||||
pod_ready = False
|
||||
pod_info = None
|
||||
|
||||
while attempts < max_attempts:
|
||||
pod_info = client.get_pod_info(pod_name)
|
||||
pod_ready = pod_info and pod_info.get("status") == SandboxStatus.RUNNING
|
||||
if pod_ready:
|
||||
break
|
||||
attempts += 1
|
||||
if attempts < max_attempts:
|
||||
logging.info(f"Waiting for Pod to be ready, attempt {attempts}/{max_attempts}")
|
||||
time.sleep(wait_seconds)
|
||||
|
||||
if not pod_ready:
|
||||
logging.warning("Timed out waiting for Pod and Service to be ready")
|
||||
client.delete_pod(pod_name)
|
||||
return None
|
||||
|
||||
service_result = client.create_service_from_yaml(service_name=service_name, selector_name=pod_name)
|
||||
if not service_result:
|
||||
client.delete_pod(pod_name)
|
||||
return None
|
||||
|
||||
max_attempts = 30
|
||||
attempts = 0
|
||||
wait_seconds = 2
|
||||
service_ready = False
|
||||
service_info = None
|
||||
|
||||
while attempts < max_attempts:
|
||||
service_info = client.get_service_info(service_name)
|
||||
if service_info and ('LoadBalancer' == service_info.get("type") and service_info.get('host')):
|
||||
service_ready = True
|
||||
elif service_info and 'ClusterIP' == service_info.get("type"):
|
||||
service_ready = True
|
||||
if service_ready:
|
||||
time.sleep(wait_seconds)
|
||||
break
|
||||
attempts += 1
|
||||
if attempts < max_attempts:
|
||||
logging.info(f"Waiting for Service to be ready, attempt {attempts}/{max_attempts}")
|
||||
time.sleep(wait_seconds)
|
||||
|
||||
if not service_ready:
|
||||
client.delete_pod(pod_name)
|
||||
client.delete_service(service_name)
|
||||
return None
|
||||
|
||||
if pod_ready and service_ready:
|
||||
if mcp_servers:
|
||||
try:
|
||||
metadata = {
|
||||
"pod_name": pod_name,
|
||||
"service_name": service_name,
|
||||
"status": pod_info.get("status"),
|
||||
"cluster_ip": service_info.get("cluster_ip"),
|
||||
"host": service_info.get("host"),
|
||||
}
|
||||
|
||||
response = cls._get_mcp_configs(
|
||||
mcp_servers=mcp_servers,
|
||||
mcp_config=mcp_config,
|
||||
metadata=metadata,
|
||||
env_type=SandboxEnvType.K8S
|
||||
)
|
||||
|
||||
if response:
|
||||
mcp_config = response
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to get mcp configs: {e}")
|
||||
|
||||
return SandboxK8sResponse(
|
||||
pod_name=pod_name,
|
||||
service_name=service_name,
|
||||
status=pod_info.get("status"),
|
||||
cluster_ip=service_info.get("cluster_ip"),
|
||||
host=service_info.get("host"),
|
||||
mcp_config=mcp_config,
|
||||
env_type=SandboxEnvType.K8S,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.info(f"Failed to create Sandbox by k8s: {e}")
|
||||
# Only attempt to delete resources if client has been initialized
|
||||
if client:
|
||||
if pod_name:
|
||||
client.delete_pod(pod_name)
|
||||
if service_name:
|
||||
client.delete_service(service_name)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def generate_random_string(cls, length=6):
|
||||
"""
|
||||
Generate a random string of specified length.
|
||||
"""
|
||||
characters = string.ascii_lowercase + string.digits
|
||||
return ''.join(random.choice(characters) for _ in range(length))
|
||||
|
||||
@classmethod
|
||||
def _get_mcp_configs(
|
||||
cls,
|
||||
mcp_servers: Optional[List[str]] = None,
|
||||
mcp_config: Optional[Any] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
env_type: Optional[int] = None,
|
||||
) -> Any:
|
||||
"""
|
||||
Get MCP configurations for the sandbox.
|
||||
"""
|
||||
try:
|
||||
if not metadata or (
|
||||
not metadata.get("cluster_ip") and not metadata.get("host")):
|
||||
return mcp_config
|
||||
host = metadata.get("host") or metadata.get("cluster_ip")
|
||||
|
||||
if not mcp_servers:
|
||||
return None
|
||||
if not mcp_config or mcp_config.get("mcpServers") is None:
|
||||
mcp_config = {
|
||||
"mcpServers": {}
|
||||
}
|
||||
_mcp_servers = mcp_config.get("mcpServers")
|
||||
|
||||
for server in mcp_servers:
|
||||
if server not in _mcp_servers:
|
||||
_mcp_servers[server] = {
|
||||
"type": "api",
|
||||
"url": f"http://{host}:80/{server}"
|
||||
}
|
||||
|
||||
return mcp_config
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to get_mcp_configs_from_k8s: {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def _remove_sandbox(
|
||||
cls,
|
||||
sandbox_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
env_type: Optional[int] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Remove the Kubernetes sandbox and clean up resources.
|
||||
"""
|
||||
try:
|
||||
if not sandbox_id or not metadata:
|
||||
logging.warning(f"sandbox_id={sandbox_id} or metadata={metadata} is None")
|
||||
return False
|
||||
|
||||
pod_name = metadata.get("pod_name")
|
||||
service_name = metadata.get("service_name")
|
||||
|
||||
if not pod_name or not service_name:
|
||||
logging.warning(f"pod_name={pod_name} or service_name={service_name} is None")
|
||||
return False
|
||||
|
||||
client = KubernetesApiClient()
|
||||
client.delete_pod(pod_name)
|
||||
client.delete_service(service_name)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to remove Sandbox: {e}")
|
||||
return False
|
||||
@@ -0,0 +1,69 @@
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from aworld.sandbox.api.base_sandbox_api import BaseSandboxApi
|
||||
from aworld.sandbox.models import SandboxStatus, SandboxEnvType, SandboxLocalResponse
|
||||
from aworld.sandbox.run.mcp_servers import McpServers
|
||||
|
||||
|
||||
class LocalSandboxApi(BaseSandboxApi):
|
||||
"""
|
||||
API implementation for local sandbox operations.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _create_sandbox(
|
||||
cls,
|
||||
env_type: int,
|
||||
env_config: Any,
|
||||
mcp_servers: Optional[List[str]] = None,
|
||||
mcp_config: Optional[Any] = None,
|
||||
black_tool_actions: Optional[Dict[str, List[str]]] = None
|
||||
) -> SandboxLocalResponse:
|
||||
"""
|
||||
Create a local sandbox based on the reference implementation.
|
||||
"""
|
||||
try:
|
||||
if not mcp_servers:
|
||||
logging.info("_create_sandbox_by_local mcp_servers is not exist")
|
||||
return None
|
||||
|
||||
return SandboxLocalResponse(
|
||||
status=SandboxStatus.RUNNING,
|
||||
mcp_config=mcp_config,
|
||||
env_type=SandboxEnvType.LOCAL
|
||||
)
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to create local sandbox: {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _get_mcp_configs(
|
||||
cls,
|
||||
mcp_servers: Optional[List[str]] = None,
|
||||
mcp_config: Optional[Any] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
env_type: Optional[int] = None,
|
||||
) -> Any:
|
||||
"""
|
||||
Get MCP configurations for the sandbox.
|
||||
"""
|
||||
try:
|
||||
# Create McpServers instance
|
||||
return mcp_config
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to get_mcp_configs: {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def _remove_sandbox(
|
||||
cls,
|
||||
sandbox_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
env_type: Optional[int] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Remove the local sandbox.
|
||||
"""
|
||||
# Local sandbox doesn't need special removal
|
||||
return True
|
||||
@@ -0,0 +1,13 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class SandboxSetup(ABC):
|
||||
|
||||
|
||||
default_sandbox_timeout = 3000
|
||||
default_template = "api"
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def sandbox_id(self) -> str:
|
||||
...
|
||||
@@ -0,0 +1,114 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from aworld.sandbox.api.base_sandbox_api import BaseSandboxApi
|
||||
from aworld.sandbox.models import SandboxStatus, SandboxEnvType, SandboxSuperResponse
|
||||
from aworld.sandbox.run.mcp_servers import McpServers
|
||||
|
||||
|
||||
class SuperSandboxApi(BaseSandboxApi):
|
||||
"""
|
||||
API implementation for supercomputer sandbox operations.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _create_sandbox(
|
||||
cls,
|
||||
env_type: int,
|
||||
env_config: Any,
|
||||
mcp_servers: Optional[List[str]] = None,
|
||||
mcp_config: Optional[Any] = None,
|
||||
) -> SandboxSuperResponse:
|
||||
"""
|
||||
Create a supercomputer sandbox based on the reference implementation.
|
||||
"""
|
||||
try:
|
||||
if not mcp_servers:
|
||||
logging.info("_create_sandbox_by_super mcp_servers is not exist")
|
||||
return None
|
||||
|
||||
load_dotenv()
|
||||
host = os.getenv("SUPERCOMPUTER_HOST")
|
||||
|
||||
if not host:
|
||||
logging.warning("_create_sandbox_by_super SUPERCOMPUTER_HOST is null")
|
||||
return None
|
||||
|
||||
metadata = {
|
||||
"status": SandboxStatus.RUNNING,
|
||||
"host": host,
|
||||
}
|
||||
|
||||
response = cls._get_mcp_configs(
|
||||
mcp_servers=mcp_servers,
|
||||
mcp_config=mcp_config,
|
||||
metadata=metadata,
|
||||
env_type=SandboxEnvType.SUPERCOMPUTER
|
||||
)
|
||||
|
||||
if not response:
|
||||
return None
|
||||
|
||||
return SandboxSuperResponse(
|
||||
status=SandboxStatus.RUNNING,
|
||||
host=host,
|
||||
mcp_config=mcp_config,
|
||||
env_type=SandboxEnvType.SUPERCOMPUTER
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to create supercomputer sandbox: {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _get_mcp_configs(
|
||||
cls,
|
||||
mcp_servers: Optional[List[str]] = None,
|
||||
mcp_config: Optional[Any] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
env_type: Optional[int] = None,
|
||||
) -> Any:
|
||||
"""
|
||||
Get MCP configurations for the sandbox.
|
||||
"""
|
||||
try:
|
||||
if not metadata or not metadata.get("host"):
|
||||
return mcp_config
|
||||
host = metadata.get("host")
|
||||
|
||||
if not mcp_servers:
|
||||
return None
|
||||
if not mcp_config or mcp_config.get("mcpServers") is None:
|
||||
mcp_config = {
|
||||
"mcpServers": {}
|
||||
}
|
||||
_mcp_servers = mcp_config.get("mcpServers")
|
||||
|
||||
for server in mcp_servers:
|
||||
if server not in _mcp_servers:
|
||||
_mcp_servers[server] = {
|
||||
"type": "sse",
|
||||
"url": f"{host}/{server}/sse"
|
||||
}
|
||||
|
||||
return mcp_config
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to get_mcp_configs_from_super: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@classmethod
|
||||
async def _remove_sandbox(
|
||||
cls,
|
||||
sandbox_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
env_type: Optional[int] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Remove the supercomputer sandbox.
|
||||
"""
|
||||
# Supercomputer sandbox doesn't need special removal
|
||||
return True
|
||||
@@ -0,0 +1,166 @@
|
||||
import abc
|
||||
import asyncio
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from aworld.sandbox.api.setup import SandboxSetup
|
||||
from aworld.sandbox.models import SandboxStatus, SandboxEnvType, SandboxInfo
|
||||
from aworld.sandbox.run.mcp_servers import McpServers
|
||||
|
||||
|
||||
class Sandbox(SandboxSetup):
|
||||
"""
|
||||
Sandbox abstract base class that defines the interface for all sandbox implementations.
|
||||
A sandbox provides an isolated environment for executing code and operations.
|
||||
"""
|
||||
|
||||
default_sandbox_timeout = 3000
|
||||
|
||||
@property
|
||||
def sandbox_id(self) -> str:
|
||||
"""
|
||||
Returns the unique identifier of the sandbox.
|
||||
"""
|
||||
return self._sandbox_id
|
||||
|
||||
@property
|
||||
def status(self) -> SandboxStatus:
|
||||
"""
|
||||
Returns the current status of the sandbox.
|
||||
"""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def timeout(self) -> int:
|
||||
"""
|
||||
Returns the timeout value for sandbox operations.
|
||||
"""
|
||||
return self._timeout
|
||||
|
||||
@property
|
||||
def metadata(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Returns the sandbox metadata.
|
||||
"""
|
||||
return self._metadata
|
||||
|
||||
@property
|
||||
def env_type(self) -> SandboxEnvType:
|
||||
"""
|
||||
Returns the environment type of the sandbox.
|
||||
"""
|
||||
return self._env_type
|
||||
|
||||
@property
|
||||
def mcp_config(self) -> Any:
|
||||
"""
|
||||
Returns the MCP configuration.
|
||||
"""
|
||||
return self._mcp_config
|
||||
|
||||
@property
|
||||
def mcp_servers(self) -> List[str]:
|
||||
"""
|
||||
Returns the list of MCP servers.
|
||||
"""
|
||||
return self._mcp_servers
|
||||
|
||||
@property
|
||||
def black_tool_actions(self) -> Dict[str, List[str]]:
|
||||
"""
|
||||
Returns the list of black-listed tools.
|
||||
"""
|
||||
return self._black_tool_actions
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def mcpservers(self) -> McpServers:
|
||||
"""
|
||||
Module for running MCP in the sandbox.
|
||||
|
||||
Returns:
|
||||
McpServers: The MCP servers instance.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sandbox_id: Optional[str] = None,
|
||||
env_type: Optional[int] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
timeout: Optional[int] = None,
|
||||
mcp_servers: Optional[List[str]] = None,
|
||||
mcp_config: Optional[Any] = None,
|
||||
black_tool_actions: Optional[Dict[str, List[str]]] = None
|
||||
):
|
||||
"""
|
||||
Initialize a new Sandbox instance.
|
||||
|
||||
Args:
|
||||
sandbox_id: Unique identifier for the sandbox. If None, one will be generated.
|
||||
env_type: The environment type (LOCAL, K8S, SUPERCOMPUTER).
|
||||
metadata: Additional metadata for the sandbox.
|
||||
timeout: Timeout for sandbox operations.
|
||||
mcp_servers: List of MCP servers to use.
|
||||
mcp_config: Configuration for MCP servers.
|
||||
"""
|
||||
# Initialize basic attributes
|
||||
self._sandbox_id = sandbox_id or str(uuid.uuid4())
|
||||
self._status = SandboxStatus.INIT
|
||||
self._timeout = timeout or self.default_sandbox_timeout
|
||||
self._metadata = metadata or {}
|
||||
self._env_type = env_type or SandboxEnvType.LOCAL
|
||||
self._mcp_servers = mcp_servers or []
|
||||
self._mcp_config = mcp_config or {}
|
||||
self._black_tool_actions = black_tool_actions or {}
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_info(self) -> SandboxInfo:
|
||||
"""
|
||||
Returns information about the sandbox.
|
||||
|
||||
Returns:
|
||||
SandboxInfo: Information about the sandbox.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def remove(self) -> bool:
|
||||
"""
|
||||
Remove the sandbox and clean up all resources.
|
||||
|
||||
Returns:
|
||||
bool: True if removal was successful, False otherwise.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def cleanup(self) -> bool:
|
||||
"""
|
||||
Clean up the sandbox resources.
|
||||
|
||||
Returns:
|
||||
bool: True if cleanup was successful, False otherwise.
|
||||
"""
|
||||
pass
|
||||
|
||||
def __del__(self):
|
||||
"""
|
||||
Ensure resources are cleaned up when the object is garbage collected.
|
||||
"""
|
||||
try:
|
||||
# Handle the case where an event loop already exists
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
logging.warning("Cannot clean up sandbox in __del__ when event loop is already running")
|
||||
return
|
||||
except RuntimeError:
|
||||
# No running event loop, create a new one
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(self.cleanup())
|
||||
loop.close()
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to cleanup sandbox resources during garbage collection: {e}")
|
||||
@@ -0,0 +1,106 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import abc
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from aworld.sandbox.base import Sandbox
|
||||
from aworld.sandbox.models import SandboxStatus, SandboxEnvType, SandboxInfo
|
||||
from aworld.sandbox.run.mcp_servers import McpServers
|
||||
|
||||
|
||||
class BaseSandbox(Sandbox):
|
||||
"""
|
||||
Base sandbox implementation with common functionality for all sandbox types.
|
||||
This class implements common methods and provides a foundation for specific sandbox implementations.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sandbox_id: Optional[str] = None,
|
||||
env_type: Optional[int] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
timeout: Optional[int] = None,
|
||||
mcp_servers: Optional[List[str]] = None,
|
||||
mcp_config: Optional[Any] = None,
|
||||
black_tool_actions: Optional[Dict[str, List[str]]] = None
|
||||
):
|
||||
"""
|
||||
Initialize a new BaseSandbox instance.
|
||||
|
||||
Args:
|
||||
sandbox_id: Unique identifier for the sandbox. If None, one will be generated.
|
||||
env_type: The environment type (LOCAL, K8S, SUPERCOMPUTER).
|
||||
metadata: Additional metadata for the sandbox.
|
||||
timeout: Timeout for sandbox operations.
|
||||
mcp_servers: List of MCP servers to use.
|
||||
mcp_config: Configuration for MCP servers.
|
||||
"""
|
||||
super().__init__(
|
||||
sandbox_id=sandbox_id,
|
||||
env_type=env_type,
|
||||
metadata=metadata,
|
||||
timeout=timeout,
|
||||
mcp_servers=mcp_servers,
|
||||
mcp_config=mcp_config,
|
||||
black_tool_actions=black_tool_actions
|
||||
)
|
||||
self._logger = self._setup_logger()
|
||||
|
||||
def _setup_logger(self):
|
||||
"""
|
||||
Set up a logger for the sandbox instance.
|
||||
|
||||
Returns:
|
||||
logging.Logger: Configured logger instance.
|
||||
"""
|
||||
logger = logging.getLogger(f"sandbox.{self.__class__.__name__}.{self.sandbox_id[:8]}")
|
||||
return logger
|
||||
|
||||
def get_info(self) -> SandboxInfo:
|
||||
"""
|
||||
Get information about the sandbox.
|
||||
|
||||
Returns:
|
||||
SandboxInfo: Information about the sandbox.
|
||||
"""
|
||||
return {
|
||||
"sandbox_id": self.sandbox_id,
|
||||
"status": self.status,
|
||||
"metadata": self.metadata,
|
||||
"env_type": self.env_type
|
||||
}
|
||||
|
||||
@property
|
||||
def mcpservers(self) -> McpServers:
|
||||
"""
|
||||
Module for running MCP servers in the sandbox.
|
||||
This property provides access to the MCP servers instance.
|
||||
|
||||
Returns:
|
||||
McpServers: The MCP servers instance.
|
||||
"""
|
||||
if hasattr(self, '_mcpservers'):
|
||||
return self._mcpservers
|
||||
return None
|
||||
|
||||
@abc.abstractmethod
|
||||
async def cleanup(self) -> bool:
|
||||
"""
|
||||
Clean up sandbox resources.
|
||||
This method must be implemented by subclasses to provide environment-specific cleanup.
|
||||
|
||||
Returns:
|
||||
bool: True if cleanup was successful, False otherwise.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def remove(self) -> bool:
|
||||
"""
|
||||
Remove the sandbox.
|
||||
This method must be implemented by subclasses to provide environment-specific removal.
|
||||
|
||||
Returns:
|
||||
bool: True if removal was successful, False otherwise.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,41 @@
|
||||
import asyncio
|
||||
|
||||
from aworld.sandbox import Sandbox, SandboxEnvType
|
||||
|
||||
|
||||
# Define an asynchronous function to call asynchronous methods
|
||||
async def run_async_tasks(sand_box):
|
||||
tools = await sand_box.mcpservers.list_tools()
|
||||
print(f"Tools: {tools}")
|
||||
return tools
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 只使用memory服务器
|
||||
mcp_servers = ["memory","amap-amap-sse"]
|
||||
mcp_config = {
|
||||
"mcpServers": {
|
||||
"memory": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-memory"
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
sand_box = Sandbox(mcp_servers=mcp_servers, mcp_config=mcp_config,env_type=SandboxEnvType.SUPERCOMPUTER)
|
||||
print(f"Sandbox ID: {sand_box.sandbox_id}")
|
||||
print(f"Status: {sand_box.status}")
|
||||
print(f"Timeout: {sand_box.timeout}")
|
||||
print(f"Metadata: {sand_box.metadata}")
|
||||
print(f"Environment Type: {sand_box.env_type}")
|
||||
print(f"MCP Servers: {sand_box.mcp_servers}")
|
||||
print(f"MCP Config: {sand_box.mcp_config}")
|
||||
|
||||
# Use asyncio to run asynchronous methods
|
||||
asyncio.run(run_async_tasks(sand_box))
|
||||
|
||||
print(f"MCP Servers-new: {sand_box.mcp_servers}")
|
||||
print(f"MCP Config-new: {sand_box.mcp_config}")
|
||||
@@ -0,0 +1,676 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
General Kubernetes API Client Example
|
||||
Can be used to perform various Kubernetes API operations
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
import yaml
|
||||
from dotenv import load_dotenv
|
||||
from kubernetes import client, config
|
||||
from kubernetes.client import V1DeleteOptions
|
||||
from kubernetes.client.rest import ApiException
|
||||
|
||||
|
||||
class KubernetesApiClient:
|
||||
"""Kubernetes API Client Wrapper Class"""
|
||||
|
||||
def __init__(self, kubeconfig_path=None, context=None, in_cluster=False):
|
||||
"""
|
||||
Initialize Kubernetes API Client
|
||||
|
||||
Args:
|
||||
kubeconfig_path: kubeconfig file path, defaults to None which uses ~/.kube/config
|
||||
context: kubeconfig context name to use
|
||||
in_cluster: whether running inside a cluster, if True use service account configuration
|
||||
"""
|
||||
try:
|
||||
# Use absolute path relative to the script file for KUBECONFIG_PATH
|
||||
load_dotenv()
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
script_path = os.path.join(script_dir, "kubeconfig")
|
||||
kubeconfig_path = kubeconfig_path or os.getenv("KUBECONFIG_PATH") or script_path
|
||||
if in_cluster:
|
||||
config.load_incluster_config()
|
||||
else:
|
||||
config.load_kube_config(
|
||||
config_file=kubeconfig_path,
|
||||
context=context
|
||||
)
|
||||
|
||||
# Initialize various API clients
|
||||
self.core_v1 = client.CoreV1Api()
|
||||
self.apps_v1 = client.AppsV1Api()
|
||||
self.batch_v1 = client.BatchV1Api()
|
||||
self.networking_v1 = client.NetworkingV1Api()
|
||||
self.rbac_v1 = client.RbacAuthorizationV1Api()
|
||||
self.custom_objects = client.CustomObjectsApi()
|
||||
|
||||
logging.info("Kubernetes API client initialized successfully")
|
||||
except Exception as e:
|
||||
logging.info(f"Failed to initialize Kubernetes API client: {e}")
|
||||
raise
|
||||
|
||||
# ===================== Pod Operations =====================
|
||||
|
||||
def get_pod(self, name, namespace="default"):
|
||||
"""
|
||||
Get a specific Pod in the given namespace
|
||||
Equivalent to: GET /api/v1/namespaces/{namespace}/pods/{name}
|
||||
"""
|
||||
try:
|
||||
return self.core_v1.read_namespaced_pod(
|
||||
name=name,
|
||||
namespace=namespace
|
||||
)
|
||||
except ApiException as e:
|
||||
logging.warning(f"Failed to get Pod {namespace}/{name}: {e}")
|
||||
return None
|
||||
|
||||
def list_pods(self, namespace="default", label_selector=None, field_selector=None):
|
||||
"""
|
||||
List all Pods in the given namespace
|
||||
Equivalent to: GET /api/v1/namespaces/{namespace}/pods
|
||||
"""
|
||||
try:
|
||||
return self.core_v1.list_namespaced_pod(
|
||||
namespace=namespace,
|
||||
label_selector=label_selector,
|
||||
field_selector=field_selector
|
||||
)
|
||||
except ApiException as e:
|
||||
logging.warning(f"Failed to list Pods in namespace {namespace}: {e}")
|
||||
return None
|
||||
|
||||
def list_pods_all_namespaces(self, label_selector=None, field_selector=None):
|
||||
"""
|
||||
List Pods across all namespaces
|
||||
Equivalent to: GET /api/v1/pods
|
||||
"""
|
||||
try:
|
||||
return self.core_v1.list_pod_for_all_namespaces(
|
||||
label_selector=label_selector,
|
||||
field_selector=field_selector
|
||||
)
|
||||
except ApiException as e:
|
||||
logging.warning(f"Failed to list Pods across all namespaces: {e}")
|
||||
return None
|
||||
|
||||
def create_pod(self, pod_manifest, namespace="default"):
|
||||
"""
|
||||
Create a Pod
|
||||
Equivalent to: POST /api/v1/namespaces/{namespace}/pods
|
||||
|
||||
Args:
|
||||
pod_manifest: Pod resource definition, can be dict or V1Pod object
|
||||
namespace: Namespace where the Pod will be created
|
||||
|
||||
Returns:
|
||||
V1Pod: The created Pod object on success
|
||||
None: On failure
|
||||
"""
|
||||
try:
|
||||
# If input is a dictionary, use it directly
|
||||
if isinstance(pod_manifest, dict):
|
||||
# Using dictionary definition
|
||||
return self.core_v1.create_namespaced_pod(
|
||||
namespace=namespace,
|
||||
body=pod_manifest
|
||||
)
|
||||
else:
|
||||
# Using V1Pod object directly
|
||||
return self.core_v1.create_namespaced_pod(
|
||||
namespace=namespace,
|
||||
body=pod_manifest
|
||||
)
|
||||
except ApiException as e:
|
||||
logging.warning(f"Failed to create Pod: {e}")
|
||||
return None
|
||||
|
||||
def create_pod_from_yaml(self, yaml_file=None, namespace=None, pod_name=None):
|
||||
"""
|
||||
Create a Pod from YAML file
|
||||
|
||||
Args:
|
||||
yaml_file: YAML file path
|
||||
namespace: Namespace where the Pod will be created
|
||||
pod_name: Override the Pod name in the YAML
|
||||
|
||||
Returns:
|
||||
V1Pod: The created Pod object on success
|
||||
None: On failure
|
||||
"""
|
||||
try:
|
||||
load_dotenv()
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
pod_path = os.path.join(script_dir, "pod.yaml")
|
||||
yaml_file = yaml_file or os.getenv("POD_YAML_PATH") or pod_path
|
||||
namespace = namespace or os.getenv("POD_NAMESPACE") or "default"
|
||||
with open(yaml_file, 'r') as f:
|
||||
pod_manifest = yaml.safe_load(f)
|
||||
# Update Pod name if provided
|
||||
if pod_name:
|
||||
if 'metadata' in pod_manifest:
|
||||
pod_manifest['metadata']['name'] = pod_name
|
||||
if 'labels' in pod_manifest['metadata']:
|
||||
pod_manifest['metadata']['labels']['name'] = pod_name
|
||||
if 'spec' in pod_manifest and 'containers' in pod_manifest['spec'] and pod_manifest['spec'][
|
||||
'containers']:
|
||||
pod_manifest['spec']['containers'][0]['name'] = pod_name
|
||||
|
||||
return self.create_pod(pod_manifest, namespace)
|
||||
except Exception as e:
|
||||
logging.info(f"Failed to create Pod from YAML file: {e}")
|
||||
return None
|
||||
|
||||
def delete_pod(self, name, namespace="default", grace_period_seconds=30):
|
||||
"""
|
||||
Delete a Pod
|
||||
Equivalent to: DELETE /api/v1/namespaces/{namespace}/pods/{name}
|
||||
|
||||
Args:
|
||||
name: Pod name
|
||||
namespace: Namespace where the Pod is located
|
||||
grace_period_seconds: Grace period in seconds
|
||||
|
||||
Returns:
|
||||
V1Status: Status object on successful deletion
|
||||
None: On failure
|
||||
"""
|
||||
try:
|
||||
return self.core_v1.delete_namespaced_pod(
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
body=V1DeleteOptions(
|
||||
grace_period_seconds=grace_period_seconds,
|
||||
propagation_policy="Background"
|
||||
)
|
||||
)
|
||||
except ApiException as e:
|
||||
logging.warning(f"Failed to delete Pod {namespace}/{name}: {e}")
|
||||
return None
|
||||
|
||||
def update_pod(self, name, pod_manifest, namespace="default"):
|
||||
"""
|
||||
Update a Pod
|
||||
Equivalent to: PUT /api/v1/namespaces/{namespace}/pods/{name}
|
||||
|
||||
Args:
|
||||
name: Pod name
|
||||
pod_manifest: Pod resource definition, can be dict or V1Pod object
|
||||
namespace: Namespace where the Pod is located
|
||||
|
||||
Returns:
|
||||
V1Pod: The updated Pod object on success
|
||||
None: On failure
|
||||
"""
|
||||
try:
|
||||
# If input is a dictionary, ensure name and namespace fields
|
||||
if isinstance(pod_manifest, dict):
|
||||
if 'metadata' not in pod_manifest:
|
||||
pod_manifest['metadata'] = {}
|
||||
pod_manifest['metadata']['name'] = name
|
||||
pod_manifest['metadata']['namespace'] = namespace
|
||||
|
||||
return self.core_v1.replace_namespaced_pod(
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
body=pod_manifest
|
||||
)
|
||||
else:
|
||||
# Ensure V1Pod object has correct name and namespace
|
||||
pod_manifest.metadata.name = name
|
||||
pod_manifest.metadata.namespace = namespace
|
||||
|
||||
return self.core_v1.replace_namespaced_pod(
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
body=pod_manifest
|
||||
)
|
||||
except ApiException as e:
|
||||
logging.warning(f"Failed to update Pod {namespace}/{name}: {e}")
|
||||
return None
|
||||
|
||||
def patch_pod(self, name, patch_data, namespace="default"):
|
||||
"""
|
||||
Partially update a Pod
|
||||
Equivalent to: PATCH /api/v1/namespaces/{namespace}/pods/{name}
|
||||
|
||||
Args:
|
||||
name: Pod name
|
||||
patch_data: Data to update, in dictionary format
|
||||
namespace: Namespace where the Pod is located
|
||||
|
||||
Returns:
|
||||
V1Pod: The updated Pod object on success
|
||||
None: On failure
|
||||
"""
|
||||
try:
|
||||
return self.core_v1.patch_namespaced_pod(
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
body=patch_data
|
||||
)
|
||||
except ApiException as e:
|
||||
logging.warning(f"Failed to patch Pod {namespace}/{name}: {e}")
|
||||
return None
|
||||
|
||||
def get_pod_info(self, name, namespace="default"):
|
||||
"""
|
||||
Get basic information about a Pod
|
||||
|
||||
Args:
|
||||
name: Pod name
|
||||
namespace: Namespace where the Pod is located
|
||||
|
||||
Returns:
|
||||
dict: Dictionary containing basic Pod information including status, IP, start time, etc.
|
||||
None: On failure
|
||||
"""
|
||||
try:
|
||||
pod = self.get_pod(name, namespace)
|
||||
if not pod:
|
||||
return None
|
||||
|
||||
# Format start time for readability
|
||||
start_time = None
|
||||
if pod.status.start_time:
|
||||
# Convert time to readable format (ISO format: YYYY-MM-DD HH:MM:SS)
|
||||
start_time_obj = pod.status.start_time.replace(tzinfo=None)
|
||||
start_time = start_time_obj.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
pod_info = {
|
||||
"pod_name": pod.metadata.name,
|
||||
"namespace": pod.metadata.namespace,
|
||||
"status": pod.status.phase, # Pending, Running, Succeeded, Failed, Unknown
|
||||
"pod_ip": pod.status.pod_ip,
|
||||
"host_ip": pod.status.host_ip,
|
||||
"start_time": start_time,
|
||||
"node_name": pod.spec.node_name if hasattr(pod.spec, "node_name") else None
|
||||
}
|
||||
|
||||
return pod_info
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to get Pod information for {namespace}/{name}: {e}")
|
||||
return None
|
||||
|
||||
# ===================== Deployment Operations =====================
|
||||
|
||||
def get_deployment(self, name, namespace="default"):
|
||||
"""
|
||||
Get a specific Deployment
|
||||
Equivalent to: GET /apis/apps/v1/namespaces/{namespace}/deployments/{name}
|
||||
"""
|
||||
try:
|
||||
return self.apps_v1.read_namespaced_deployment(
|
||||
name=name,
|
||||
namespace=namespace
|
||||
)
|
||||
except ApiException as e:
|
||||
logging.warning(f"Failed to get Deployment {namespace}/{name}: {e}")
|
||||
return None
|
||||
|
||||
def list_deployments(self, namespace="default", label_selector=None):
|
||||
"""
|
||||
List all Deployments in the given namespace
|
||||
Equivalent to: GET /apis/apps/v1/namespaces/{namespace}/deployments
|
||||
"""
|
||||
try:
|
||||
return self.apps_v1.list_namespaced_deployment(
|
||||
namespace=namespace,
|
||||
label_selector=label_selector
|
||||
)
|
||||
except ApiException as e:
|
||||
logging.warning(f"Failed to list Deployments in namespace {namespace}: {e}")
|
||||
return None
|
||||
|
||||
# ===================== Service Operations =====================
|
||||
|
||||
def get_service(self, name, namespace="default"):
|
||||
"""
|
||||
Get a specific Service
|
||||
Equivalent to: GET /api/v1/namespaces/{namespace}/services/{name}
|
||||
"""
|
||||
try:
|
||||
return self.core_v1.read_namespaced_service(
|
||||
name=name,
|
||||
namespace=namespace
|
||||
)
|
||||
except ApiException as e:
|
||||
logging.warning(f"Failed to get Service {namespace}/{name}: {e}")
|
||||
return None
|
||||
|
||||
def list_services(self, namespace="default", label_selector=None):
|
||||
"""
|
||||
List all Services in the given namespace
|
||||
Equivalent to: GET /api/v1/namespaces/{namespace}/services
|
||||
"""
|
||||
try:
|
||||
return self.core_v1.list_namespaced_service(
|
||||
namespace=namespace,
|
||||
label_selector=label_selector
|
||||
)
|
||||
except ApiException as e:
|
||||
logging.warning(f"Failed to list Services in namespace {namespace}: {e}")
|
||||
return None
|
||||
|
||||
def create_service(self, service_manifest, namespace="default"):
|
||||
"""
|
||||
Create a Service
|
||||
Equivalent to: POST /api/v1/namespaces/{namespace}/services
|
||||
|
||||
Args:
|
||||
service_manifest: Service resource definition, can be dict or V1Service object
|
||||
namespace: Namespace where the Service will be created
|
||||
|
||||
Returns:
|
||||
V1Service: The created Service object on success
|
||||
None: On failure
|
||||
"""
|
||||
try:
|
||||
# If input is a dictionary, use it directly
|
||||
if isinstance(service_manifest, dict):
|
||||
# Using dictionary definition
|
||||
return self.core_v1.create_namespaced_service(
|
||||
namespace=namespace,
|
||||
body=service_manifest
|
||||
)
|
||||
else:
|
||||
# Using V1Service object directly
|
||||
return self.core_v1.create_namespaced_service(
|
||||
namespace=namespace,
|
||||
body=service_manifest
|
||||
)
|
||||
except ApiException as e:
|
||||
logging.warning(f"Failed to create Service: {e}")
|
||||
return None
|
||||
|
||||
def create_service_from_yaml(self, yaml_file=None, namespace=None, service_name=None, selector_name=None):
|
||||
"""
|
||||
Create a Service from YAML file
|
||||
|
||||
Args:
|
||||
yaml_file: YAML file path
|
||||
namespace: Namespace where the Service will be created
|
||||
service_name: Override the Service name in the YAML
|
||||
selector_name: Override the selector name for pod targeting
|
||||
|
||||
Returns:
|
||||
V1Service: The created Service object on success
|
||||
None: On failure
|
||||
"""
|
||||
try:
|
||||
load_dotenv()
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
service_path = os.path.join(script_dir, "service.yaml")
|
||||
yaml_file = yaml_file or os.getenv("SERVICE_YAML_PATH") or service_path
|
||||
namespace = namespace or os.getenv("SERVICE_NAMESPACE") or "default"
|
||||
with open(yaml_file, 'r') as f:
|
||||
service_manifest = yaml.safe_load(f)
|
||||
|
||||
# Update Service name if provided
|
||||
if service_name:
|
||||
if 'metadata' in service_manifest:
|
||||
service_manifest['metadata']['name'] = service_name
|
||||
# Update app label if present
|
||||
if 'labels' in service_manifest['metadata']:
|
||||
service_manifest['metadata']['labels']['app'] = service_name
|
||||
|
||||
# Update selector if provided
|
||||
if selector_name:
|
||||
if 'spec' in service_manifest and 'selector' in service_manifest['spec']:
|
||||
service_manifest['spec']['selector']['name'] = selector_name
|
||||
|
||||
return self.create_service(service_manifest, namespace)
|
||||
except Exception as e:
|
||||
logging.info(f"Failed to create Service from YAML file: {e}")
|
||||
return None
|
||||
|
||||
def update_service(self, name, service_manifest, namespace="default"):
|
||||
"""
|
||||
Update a Service
|
||||
Equivalent to: PUT /api/v1/namespaces/{namespace}/services/{name}
|
||||
|
||||
Args:
|
||||
name: Service name
|
||||
service_manifest: Service resource definition, can be dict or V1Service object
|
||||
namespace: Namespace where the Service is located
|
||||
|
||||
Returns:
|
||||
V1Service: The updated Service object on success
|
||||
None: On failure
|
||||
"""
|
||||
try:
|
||||
# If input is a dictionary, ensure name and namespace fields
|
||||
if isinstance(service_manifest, dict):
|
||||
if 'metadata' not in service_manifest:
|
||||
service_manifest['metadata'] = {}
|
||||
service_manifest['metadata']['name'] = name
|
||||
service_manifest['metadata']['namespace'] = namespace
|
||||
|
||||
return self.core_v1.replace_namespaced_service(
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
body=service_manifest
|
||||
)
|
||||
else:
|
||||
# Ensure V1Service object has correct name and namespace
|
||||
service_manifest.metadata.name = name
|
||||
service_manifest.metadata.namespace = namespace
|
||||
|
||||
return self.core_v1.replace_namespaced_service(
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
body=service_manifest
|
||||
)
|
||||
except ApiException as e:
|
||||
logging.warning(f"Failed to update Service {namespace}/{name}: {e}")
|
||||
return None
|
||||
|
||||
def patch_service(self, name, patch_data, namespace="default"):
|
||||
"""
|
||||
Partially update a Service
|
||||
Equivalent to: PATCH /api/v1/namespaces/{namespace}/services/{name}
|
||||
|
||||
Args:
|
||||
name: Service name
|
||||
patch_data: Data to update, in dictionary format
|
||||
namespace: Namespace where the Service is located
|
||||
|
||||
Returns:
|
||||
V1Service: The updated Service object on success
|
||||
None: On failure
|
||||
"""
|
||||
try:
|
||||
return self.core_v1.patch_namespaced_service(
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
body=patch_data
|
||||
)
|
||||
except ApiException as e:
|
||||
logging.warning(f"Failed to patch Service {namespace}/{name}: {e}")
|
||||
return None
|
||||
|
||||
def delete_service(self, name, namespace="default"):
|
||||
"""
|
||||
Delete a Service
|
||||
Equivalent to: DELETE /api/v1/namespaces/{namespace}/services/{name}
|
||||
|
||||
Args:
|
||||
name: Service name
|
||||
namespace: Namespace where the Service is located
|
||||
|
||||
Returns:
|
||||
V1Status: Status object on successful deletion
|
||||
None: On failure
|
||||
"""
|
||||
try:
|
||||
return self.core_v1.delete_namespaced_service(
|
||||
name=name,
|
||||
namespace=namespace
|
||||
)
|
||||
except ApiException as e:
|
||||
logging.warning(f"Failed to delete Service {namespace}/{name}: {e}")
|
||||
return None
|
||||
|
||||
def get_service_info(self, name, namespace="default"):
|
||||
"""
|
||||
Get basic information about a Service
|
||||
|
||||
Args:
|
||||
name: Service name
|
||||
namespace: Namespace where the Service is located
|
||||
|
||||
Returns:
|
||||
dict: Dictionary containing basic Service information including type, IP, ports, etc.
|
||||
None: On failure
|
||||
"""
|
||||
try:
|
||||
service = self.get_service(name, namespace)
|
||||
if not service:
|
||||
return None
|
||||
|
||||
# Format creation time for readability
|
||||
creation_time = None
|
||||
if service.metadata.creation_timestamp:
|
||||
creation_time_obj = service.metadata.creation_timestamp.replace(tzinfo=None)
|
||||
creation_time = creation_time_obj.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# Simplify port information
|
||||
ports_info = []
|
||||
if service.spec.ports:
|
||||
for port in service.spec.ports:
|
||||
port_info = {
|
||||
"port": port.port,
|
||||
"target_port": port.target_port,
|
||||
"protocol": port.protocol
|
||||
}
|
||||
|
||||
# Add node port if present for NodePort type
|
||||
if hasattr(port, "node_port") and port.node_port:
|
||||
port_info["node_port"] = port.node_port
|
||||
|
||||
ports_info.append(port_info)
|
||||
|
||||
service_info = {
|
||||
"service_name": service.metadata.name,
|
||||
"namespace": service.metadata.namespace,
|
||||
"type": service.spec.type, # ClusterIP, NodePort, LoadBalancer, ExternalName
|
||||
"cluster_ip": service.spec.cluster_ip,
|
||||
"creation_time": creation_time,
|
||||
"ports": ports_info,
|
||||
"selector": service.spec.selector,
|
||||
"host": '' # Default to use cluster_ip as host
|
||||
}
|
||||
|
||||
# Add external IP information for LoadBalancer type
|
||||
if service.spec.type == "LoadBalancer" and hasattr(service.status,
|
||||
"load_balancer") and service.status.load_balancer:
|
||||
external_ips = []
|
||||
if hasattr(service.status.load_balancer, "ingress") and service.status.load_balancer.ingress:
|
||||
for ingress in service.status.load_balancer.ingress:
|
||||
if hasattr(ingress, "ip") and ingress.ip:
|
||||
external_ips.append(ingress.ip)
|
||||
elif hasattr(ingress, "hostname") and ingress.hostname:
|
||||
external_ips.append(ingress.hostname)
|
||||
|
||||
# If there are external IPs or hostnames, use the first one as the host field
|
||||
if external_ips:
|
||||
service_info["host"] = external_ips[0]
|
||||
|
||||
service_info["external_ips"] = external_ips
|
||||
|
||||
# Add external name for ExternalName type
|
||||
if service.spec.type == "ExternalName" and hasattr(service.spec, "external_name"):
|
||||
service_info["external_name"] = service.spec.external_name
|
||||
service_info["host"] = service.spec.external_name # For ExternalName type, use external_name as host
|
||||
|
||||
return service_info
|
||||
except Exception as e:
|
||||
print(f"Failed to get Service information for {namespace}/{name}: {e}")
|
||||
return None
|
||||
|
||||
# ===================== Namespace Operations =====================
|
||||
|
||||
def get_namespace(self, name):
|
||||
"""
|
||||
Get a specific namespace
|
||||
Equivalent to: GET /api/v1/namespaces/{name}
|
||||
"""
|
||||
try:
|
||||
return self.core_v1.read_namespace(name=name)
|
||||
except ApiException as e:
|
||||
logging.warning(f"Failed to get namespace {name}: {e}")
|
||||
return None
|
||||
|
||||
def list_namespaces(self, label_selector=None):
|
||||
"""
|
||||
List all namespaces
|
||||
Equivalent to: GET /api/v1/namespaces
|
||||
"""
|
||||
try:
|
||||
return self.core_v1.list_namespace(label_selector=label_selector)
|
||||
except ApiException as e:
|
||||
logging.warning(f"Failed to list namespaces: {e}")
|
||||
return None
|
||||
|
||||
# ===================== Custom Resource (CRD) Operations =====================
|
||||
|
||||
def get_custom_resource(self, group, version, plural, name, namespace=None):
|
||||
"""
|
||||
Get a custom resource
|
||||
|
||||
For namespaced resources:
|
||||
GET /apis/{group}/{version}/namespaces/{namespace}/{plural}/{name}
|
||||
|
||||
For cluster-scoped resources:
|
||||
GET /apis/{group}/{version}/{plural}/{name}
|
||||
"""
|
||||
try:
|
||||
if namespace:
|
||||
return self.custom_objects.get_namespaced_custom_object(
|
||||
group=group,
|
||||
version=version,
|
||||
namespace=namespace,
|
||||
plural=plural,
|
||||
name=name
|
||||
)
|
||||
else:
|
||||
return self.custom_objects.get_cluster_custom_object(
|
||||
group=group,
|
||||
version=version,
|
||||
plural=plural,
|
||||
name=name
|
||||
)
|
||||
except ApiException as e:
|
||||
resource_path = f"{namespace}/{name}" if namespace else name
|
||||
logging.warning(f"Failed to get custom resource {group}/{version}/{plural}/{resource_path}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
client = KubernetesApiClient("./kubeconfig")
|
||||
# result = client.get_pod("mcp-openapi-node-2", namespace="default")
|
||||
# print(result)
|
||||
#result = client.create_pod_from_yaml("./pod.yaml")
|
||||
|
||||
# result = client.get_pod_info("mcp-openapi-node-5", namespace="default")
|
||||
# print(result)
|
||||
result = client.get_service_info("mcp-openapi-service-1", namespace="default")
|
||||
print(result)
|
||||
|
||||
# result = client.create_pod_from_yaml("./pod.yaml")
|
||||
# print(result)
|
||||
|
||||
# result = client.create_service_from_yaml("./service.yaml")
|
||||
# print(result)
|
||||
|
||||
# result = client.delete_pod("mcp-openapi-node-1")
|
||||
# print(result)
|
||||
|
||||
# result = client.delete_service("mcp-openapi-service-2")
|
||||
# print(result)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
labels:
|
||||
name: mcp-openapi-node-5
|
||||
name: mcp-openapi-node-5
|
||||
spec:
|
||||
# serviceAccountName: user1 # specify specific sevice account for pod creation
|
||||
# automountServiceAccountToken: true # mount token for api access inside pod/container
|
||||
imagePullSecrets: #Comment out to enable specific image pull secret
|
||||
- name: mcp-openapi # repleace it to specific registry key
|
||||
containers:
|
||||
- image: crpi-5emlza767l7em5xz-vpc.ap-southeast-1.personal.cr.aliyuncs.com/aworld_x/mcp-openapi:0.1.5
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: mcp-openapi-node-5
|
||||
ports:
|
||||
- containerPort: 9090
|
||||
protocol: TCP
|
||||
resources: {}
|
||||
securityContext:
|
||||
capabilities: {}
|
||||
privileged: false
|
||||
terminationMessagePath: /dev/termination-log
|
||||
dnsPolicy: ClusterFirst
|
||||
restartPolicy: Always
|
||||
# nodeSelector:
|
||||
# env: test-team
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mcp-openapi-service-3 #TODO: to specify your service name
|
||||
labels:
|
||||
app: mcp-openapi-service-3
|
||||
spec:
|
||||
selector:
|
||||
name: mcp-openapi
|
||||
#name: mcp-openapi-node-1 #TODO: change label selector to match your backend pod
|
||||
ports:
|
||||
- protocol: TCP
|
||||
name: http
|
||||
port: 80 #TODO: choose an unique port on each node to avoid port conflict
|
||||
targetPort: 9090
|
||||
type: LoadBalancer
|
||||
#type: ClusterIP
|
||||
# type: LoadBalancer
|
||||
@@ -0,0 +1,5 @@
|
||||
from .local_sandbox import LocalSandbox
|
||||
from .kubernetes_sandbox import KubernetesSandbox
|
||||
from .super_sandbox import SuperSandbox
|
||||
|
||||
__all__ = ['LocalSandbox', 'KubernetesSandbox', 'SuperSandbox']
|
||||
@@ -0,0 +1,141 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import uuid
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from aworld.sandbox.common import BaseSandbox
|
||||
from aworld.sandbox.api.kubernetes.sandbox_api import KubernetesSandboxApi
|
||||
from aworld.sandbox.models import SandboxStatus, SandboxEnvType, SandboxInfo
|
||||
from aworld.sandbox.run.mcp_servers import McpServers
|
||||
|
||||
|
||||
class KubernetesSandbox(BaseSandbox, KubernetesSandboxApi):
|
||||
"""
|
||||
Kubernetes sandbox implementation that runs in a Kubernetes cluster.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sandbox_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
timeout: Optional[int] = None,
|
||||
mcp_servers: Optional[List[str]] = None,
|
||||
mcp_config: Optional[Any] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initialize a new KubernetesSandbox instance.
|
||||
|
||||
Args:
|
||||
sandbox_id: Unique identifier for the sandbox. If None, one will be generated.
|
||||
metadata: Additional metadata for the sandbox.
|
||||
timeout: Timeout for sandbox operations.
|
||||
mcp_servers: List of MCP servers to use.
|
||||
mcp_config: Configuration for MCP servers.
|
||||
**kwargs: Additional parameters for specific sandbox types.
|
||||
"""
|
||||
super().__init__(
|
||||
sandbox_id=sandbox_id,
|
||||
env_type=SandboxEnvType.K8S,
|
||||
metadata=metadata,
|
||||
timeout=timeout,
|
||||
mcp_servers=mcp_servers,
|
||||
mcp_config=mcp_config
|
||||
)
|
||||
|
||||
if sandbox_id:
|
||||
if not self._metadata:
|
||||
return self
|
||||
else:
|
||||
raise ValueError("sandbox_id is not exist")
|
||||
|
||||
# Initialize properties
|
||||
self._status = SandboxStatus.INIT
|
||||
self._timeout = timeout or self.default_sandbox_timeout
|
||||
self._metadata = metadata or {}
|
||||
self._env_type = SandboxEnvType.K8S
|
||||
self._mcp_servers = mcp_servers
|
||||
self._mcp_config = mcp_config
|
||||
|
||||
# Ensure sandbox_id has a value in all cases
|
||||
self._sandbox_id = sandbox_id or str(uuid.uuid4())
|
||||
|
||||
# If no sandbox_id provided, create a new sandbox
|
||||
if not sandbox_id:
|
||||
response = self._create_sandbox(
|
||||
env_type=self._env_type,
|
||||
env_config=None,
|
||||
mcp_servers=mcp_servers,
|
||||
mcp_config=mcp_config,
|
||||
)
|
||||
|
||||
if not response:
|
||||
self._status = SandboxStatus.ERROR
|
||||
# If creation fails, keep the generated UUID as the ID
|
||||
logging.warning(f"Failed to create K8s sandbox, using generated ID: {self._sandbox_id}")
|
||||
else:
|
||||
self._sandbox_id = response.sandbox_id
|
||||
self._status = SandboxStatus.RUNNING
|
||||
self._metadata = {
|
||||
"pod_name": getattr(response, 'pod_name', None),
|
||||
"service_name": getattr(response, 'service_name', None),
|
||||
"status": getattr(response, 'status', None),
|
||||
"cluster_ip": getattr(response, 'cluster_ip', None),
|
||||
"host": getattr(response, 'host', None),
|
||||
"mcp_config": getattr(response, 'mcp_config', None),
|
||||
"env_type": getattr(response, 'env_type', None),
|
||||
}
|
||||
self._mcp_config = getattr(response, 'mcp_config', None)
|
||||
|
||||
# Initialize McpServers
|
||||
self._mcpservers = McpServers(
|
||||
mcp_servers,
|
||||
self._mcp_config,
|
||||
sandbox=self
|
||||
)
|
||||
|
||||
async def remove(self) -> None:
|
||||
"""
|
||||
Remove sandbox.
|
||||
"""
|
||||
await self._remove_sandbox(
|
||||
sandbox_id=self.sandbox_id,
|
||||
metadata=self._metadata,
|
||||
env_type=self._env_type
|
||||
)
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
"""
|
||||
Clean up Sandbox resources, including MCP server connections
|
||||
"""
|
||||
try:
|
||||
if hasattr(self, '_mcpservers') and self._mcpservers:
|
||||
await self._mcpservers.cleanup()
|
||||
logging.info(f"Cleaned up MCP servers for sandbox {self.sandbox_id}")
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to cleanup MCP servers: {e}")
|
||||
|
||||
# Call the original remove method
|
||||
try:
|
||||
await self.remove()
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to remove sandbox: {e}")
|
||||
|
||||
def __del__(self):
|
||||
"""
|
||||
Ensure resources are cleaned up when the object is garbage collected
|
||||
"""
|
||||
try:
|
||||
# Handle the case where an event loop already exists
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
logging.warning("Cannot clean up sandbox in __del__ when event loop is already running")
|
||||
return
|
||||
except RuntimeError:
|
||||
# No running event loop, create a new one
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(self.cleanup())
|
||||
loop.close()
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to cleanup sandbox resources during garbage collection: {e}")
|
||||
@@ -0,0 +1,143 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import uuid
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from aworld.sandbox.api.local.sandbox_api import LocalSandboxApi
|
||||
from aworld.sandbox.models import SandboxStatus, SandboxEnvType, SandboxInfo
|
||||
from aworld.sandbox.run.mcp_servers import McpServers
|
||||
from aworld.sandbox.common import BaseSandbox
|
||||
|
||||
|
||||
class LocalSandbox(BaseSandbox, LocalSandboxApi):
|
||||
"""
|
||||
Local sandbox implementation that runs in the local environment.
|
||||
This sandbox runs processes and MCP servers directly on the local machine.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sandbox_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
timeout: Optional[int] = None,
|
||||
mcp_servers: Optional[List[str]] = None,
|
||||
mcp_config: Optional[Any] = None,
|
||||
black_tool_actions: Optional[Dict[str, List[str]]] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initialize a new LocalSandbox instance.
|
||||
|
||||
Args:
|
||||
sandbox_id: Unique identifier for the sandbox. If None, one will be generated.
|
||||
metadata: Additional metadata for the sandbox.
|
||||
timeout: Timeout for sandbox operations.
|
||||
mcp_servers: List of MCP servers to use.
|
||||
mcp_config: Configuration for MCP servers.
|
||||
**kwargs: Additional parameters for specific sandbox types.
|
||||
"""
|
||||
super().__init__(
|
||||
sandbox_id=sandbox_id,
|
||||
env_type=SandboxEnvType.LOCAL,
|
||||
metadata=metadata,
|
||||
timeout=timeout,
|
||||
mcp_servers=mcp_servers,
|
||||
mcp_config=mcp_config,
|
||||
black_tool_actions=black_tool_actions
|
||||
)
|
||||
|
||||
if sandbox_id:
|
||||
if not self._metadata:
|
||||
return self
|
||||
else:
|
||||
raise ValueError("sandbox_id is not exist")
|
||||
|
||||
# Initialize properties
|
||||
self._status = SandboxStatus.INIT
|
||||
self._timeout = timeout or self.default_sandbox_timeout
|
||||
self._metadata = metadata or {}
|
||||
self._env_type = SandboxEnvType.LOCAL
|
||||
self._mcp_servers = mcp_servers
|
||||
self._mcp_config = mcp_config
|
||||
self._black_tool_actions = black_tool_actions or {}
|
||||
|
||||
# Ensure sandbox_id has a value in all cases
|
||||
self._sandbox_id = sandbox_id or str(uuid.uuid4())
|
||||
|
||||
# If no sandbox_id provided, create a new sandbox
|
||||
if not sandbox_id:
|
||||
response = self._create_sandbox(
|
||||
env_type=self._env_type,
|
||||
env_config=None,
|
||||
mcp_servers=mcp_servers,
|
||||
mcp_config=mcp_config,
|
||||
black_tool_actions=black_tool_actions
|
||||
)
|
||||
|
||||
if not response:
|
||||
self._status = SandboxStatus.ERROR
|
||||
# If creation fails, keep the generated UUID as the ID
|
||||
logging.warning(f"Failed to create sandbox, using generated ID: {self._sandbox_id}")
|
||||
else:
|
||||
self._sandbox_id = response.sandbox_id
|
||||
self._status = SandboxStatus.RUNNING
|
||||
self._metadata = {
|
||||
"status": getattr(response, 'status', None),
|
||||
"mcp_config": getattr(response, 'mcp_config', None),
|
||||
"env_type": getattr(response, 'env_type', None),
|
||||
}
|
||||
self._mcp_config = getattr(response, 'mcp_config', None)
|
||||
|
||||
# Initialize McpServers with a reference to this sandbox instance
|
||||
self._mcpservers = McpServers(
|
||||
mcp_servers,
|
||||
self._mcp_config,
|
||||
sandbox=self,
|
||||
black_tool_actions=self._black_tool_actions
|
||||
)
|
||||
|
||||
async def remove(self) -> None:
|
||||
"""
|
||||
Remove sandbox.
|
||||
"""
|
||||
await self._remove_sandbox(
|
||||
sandbox_id=self.sandbox_id,
|
||||
metadata=self._metadata,
|
||||
env_type=self._env_type
|
||||
)
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
"""
|
||||
Clean up Sandbox resources, including MCP server connections
|
||||
"""
|
||||
try:
|
||||
if hasattr(self, '_mcpservers') and self._mcpservers:
|
||||
await self._mcpservers.cleanup()
|
||||
logging.info(f"Cleaned up MCP servers for sandbox {self.sandbox_id}")
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to cleanup MCP servers: {e}")
|
||||
|
||||
# Call the original remove method
|
||||
try:
|
||||
await self.remove()
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to remove sandbox: {e}")
|
||||
|
||||
def __del__(self):
|
||||
"""
|
||||
Ensure resources are cleaned up when the object is garbage collected
|
||||
"""
|
||||
try:
|
||||
# Handle the case where an event loop already exists
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
logging.warning("Cannot clean up sandbox in __del__ when event loop is already running")
|
||||
return
|
||||
except RuntimeError:
|
||||
# No running event loop, create a new one
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(self.cleanup())
|
||||
loop.close()
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to cleanup sandbox resources during garbage collection: {e}")
|
||||
@@ -0,0 +1,138 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import uuid
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
from aworld.sandbox.common import BaseSandbox
|
||||
from aworld.sandbox.api.super.sandbox_api import SuperSandboxApi
|
||||
from aworld.sandbox.models import SandboxStatus, SandboxEnvType, SandboxInfo
|
||||
from aworld.sandbox.run.mcp_servers import McpServers
|
||||
|
||||
|
||||
class SuperSandbox(BaseSandbox, SuperSandboxApi):
|
||||
"""
|
||||
Supercomputer sandbox implementation that runs on a supercomputer environment.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sandbox_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
timeout: Optional[int] = None,
|
||||
mcp_servers: Optional[List[str]] = None,
|
||||
mcp_config: Optional[Any] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initialize a new SuperSandbox instance.
|
||||
|
||||
Args:
|
||||
sandbox_id: Unique identifier for the sandbox. If None, one will be generated.
|
||||
metadata: Additional metadata for the sandbox.
|
||||
timeout: Timeout for sandbox operations.
|
||||
mcp_servers: List of MCP servers to use.
|
||||
mcp_config: Configuration for MCP servers.
|
||||
**kwargs: Additional parameters for specific sandbox types.
|
||||
"""
|
||||
super().__init__(
|
||||
sandbox_id=sandbox_id,
|
||||
env_type=SandboxEnvType.SUPERCOMPUTER,
|
||||
metadata=metadata,
|
||||
timeout=timeout,
|
||||
mcp_servers=mcp_servers,
|
||||
mcp_config=mcp_config
|
||||
)
|
||||
|
||||
if sandbox_id:
|
||||
if not self._metadata:
|
||||
return self
|
||||
else:
|
||||
raise ValueError("sandbox_id is not exist")
|
||||
|
||||
# Initialize properties
|
||||
self._status = SandboxStatus.INIT
|
||||
self._timeout = timeout or self.default_sandbox_timeout
|
||||
self._metadata = metadata or {}
|
||||
self._env_type = SandboxEnvType.SUPERCOMPUTER
|
||||
self._mcp_servers = mcp_servers
|
||||
self._mcp_config = mcp_config
|
||||
|
||||
# Ensure sandbox_id has a value in all cases
|
||||
self._sandbox_id = sandbox_id or str(uuid.uuid4())
|
||||
|
||||
# If no sandbox_id provided, create a new sandbox
|
||||
if not sandbox_id:
|
||||
response = self._create_sandbox(
|
||||
env_type=self._env_type,
|
||||
env_config=None,
|
||||
mcp_servers=mcp_servers,
|
||||
mcp_config=mcp_config,
|
||||
)
|
||||
|
||||
if not response:
|
||||
self._status = SandboxStatus.ERROR
|
||||
# If creation fails, keep the generated UUID as the ID
|
||||
logging.warning(f"Failed to create super sandbox, using generated ID: {self._sandbox_id}")
|
||||
else:
|
||||
self._sandbox_id = response.sandbox_id
|
||||
self._status = SandboxStatus.RUNNING
|
||||
self._metadata = {
|
||||
"status": getattr(response, 'status', None),
|
||||
"host": getattr(response, 'host', None),
|
||||
"mcp_config": getattr(response, 'mcp_config', None),
|
||||
"env_type": getattr(response, 'env_type', None),
|
||||
}
|
||||
self._mcp_config = getattr(response, 'mcp_config', None)
|
||||
|
||||
# Initialize McpServers
|
||||
self._mcpservers = McpServers(
|
||||
mcp_servers,
|
||||
self._mcp_config,
|
||||
sandbox=self
|
||||
)
|
||||
|
||||
async def remove(self) -> None:
|
||||
"""
|
||||
Remove sandbox.
|
||||
"""
|
||||
await self._remove_sandbox(
|
||||
sandbox_id=self.sandbox_id,
|
||||
metadata=self._metadata,
|
||||
env_type=self._env_type
|
||||
)
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
"""
|
||||
Clean up Sandbox resources, including MCP server connections
|
||||
"""
|
||||
try:
|
||||
if hasattr(self, '_mcpservers') and self._mcpservers:
|
||||
await self._mcpservers.cleanup()
|
||||
logging.info(f"Cleaned up MCP servers for sandbox {self.sandbox_id}")
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to cleanup MCP servers: {e}")
|
||||
|
||||
# Call the original remove method
|
||||
try:
|
||||
await self.remove()
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to remove sandbox: {e}")
|
||||
|
||||
def __del__(self):
|
||||
"""
|
||||
Ensure resources are cleaned up when the object is garbage collected
|
||||
"""
|
||||
try:
|
||||
# Handle the case where an event loop already exists
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
logging.warning("Cannot clean up sandbox in __del__ when event loop is already running")
|
||||
return
|
||||
except RuntimeError:
|
||||
# No running event loop, create a new one
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(self.cleanup())
|
||||
loop.close()
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to cleanup sandbox resources during garbage collection: {e}")
|
||||
@@ -0,0 +1,66 @@
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import Optional, Dict,Any
|
||||
|
||||
|
||||
class SandboxStatus(Enum):
|
||||
"""Sandbox status enumeration."""
|
||||
INIT = 'Pending' # Initialization state
|
||||
RUNNING = 'Running' # Running
|
||||
STOPPED = 'Stopped' # Stopped
|
||||
ERROR = 'Failed' # Error state
|
||||
REMOVED = 'Removed' # Removed
|
||||
UNKNOWN = 'Unknown' # Removed
|
||||
|
||||
class SandboxEnvType(Enum):
|
||||
"""Sandbox env type enumeration."""
|
||||
LOCAL = 1
|
||||
K8S = 2
|
||||
SUPERCOMPUTER = 3
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class SandboxCreateResponse:
|
||||
sandbox_id: str = str(uuid.uuid4())
|
||||
env_type: int = SandboxEnvType.LOCAL
|
||||
status: Optional[str] = None
|
||||
mcp_config: Optional[Any] = None
|
||||
|
||||
@dataclass
|
||||
class SandboxK8sResponse(SandboxCreateResponse):
|
||||
pod_name: Optional[str] = None
|
||||
service_name: Optional[str] = None
|
||||
cluster_ip: Optional[str] = None
|
||||
host: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SandboxLocalResponse(SandboxCreateResponse):
|
||||
host: Optional[str] = None
|
||||
|
||||
@dataclass
|
||||
class SandboxSuperResponse(SandboxCreateResponse):
|
||||
host: Optional[str] = None
|
||||
|
||||
@dataclass
|
||||
class SandboxInfo:
|
||||
"""Information about a sandbox."""
|
||||
|
||||
sandbox_id: str
|
||||
"""Sandbox ID."""
|
||||
status: str
|
||||
"""sandbox status"""
|
||||
metadata: Dict[str, str]
|
||||
"""Saved sandbox metadata."""
|
||||
|
||||
|
||||
|
||||
class EnvConfig(BaseModel):
|
||||
"""Data structure contained in the environment"""
|
||||
name: str = "default"
|
||||
version: str = "1.0.0"
|
||||
dockerfile: Optional[str] = None #Dockerfile of the image required when creating the environment
|
||||
@@ -0,0 +1,335 @@
|
||||
import logging
|
||||
import json
|
||||
import traceback
|
||||
|
||||
from aworld.core.context.base import Context
|
||||
# from fastmcp.server.middleware import Middleware, MiddlewareContext
|
||||
|
||||
|
||||
from aworld.utils.common import sync_exec
|
||||
|
||||
from aworld.events.util import send_message
|
||||
|
||||
from aworld.core.event.base import Message, Constants
|
||||
from typing_extensions import Optional, List, Dict, Any
|
||||
|
||||
from aworld.mcp_client.utils import mcp_tool_desc_transform, call_api, get_server_instance, cleanup_server, \
|
||||
call_function_tool, mcp_tool_desc_transform_v2
|
||||
from mcp.types import TextContent, ImageContent
|
||||
|
||||
from aworld.core.common import ActionResult
|
||||
from aworld.output import Output
|
||||
|
||||
|
||||
class McpServers:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mcp_servers: Optional[List[str]] = None,
|
||||
mcp_config: Dict[str, Any] = None,
|
||||
sandbox=None,
|
||||
black_tool_actions: Dict[str, List[str]] = None,
|
||||
) -> None:
|
||||
self.mcp_servers = mcp_servers
|
||||
self.mcp_config = mcp_config
|
||||
self.sandbox = sandbox
|
||||
# Dictionary to store server instances {server_name: server_instance}
|
||||
self.server_instances = {}
|
||||
self.tool_list = None
|
||||
self.black_tool_actions = black_tool_actions or {}
|
||||
|
||||
async def list_tools(self, context: Context = None) -> List[Dict[str, Any]]:
|
||||
if self.tool_list:
|
||||
return self.tool_list
|
||||
if not self.mcp_servers or not self.mcp_config:
|
||||
return []
|
||||
try:
|
||||
#self.tool_list = await mcp_tool_desc_transform(self.mcp_servers, self.mcp_config)
|
||||
self.tool_list = await mcp_tool_desc_transform_v2(self.mcp_servers, self.mcp_config,context,self.server_instances,self.black_tool_actions)
|
||||
return self.tool_list
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
logging.warning(f"Failed to list tools: {e}")
|
||||
return []
|
||||
|
||||
async def check_tool_params(self, context: Context, server_name: str, tool_name: str,
|
||||
parameter: Dict[str, Any]) -> Any:
|
||||
"""
|
||||
Check tool parameters and automatically supplement session_id, task_id and other parameters from context
|
||||
|
||||
Args:
|
||||
context: Context object containing session_id, task_id and other information
|
||||
server_name: Server name
|
||||
tool_name: Tool name
|
||||
parameter: Parameter dictionary, will be modified
|
||||
|
||||
Returns:
|
||||
bool: Whether parameter check passed
|
||||
"""
|
||||
# Ensure tool_list is loaded
|
||||
if not self.tool_list or not context:
|
||||
return False
|
||||
|
||||
if not self.mcp_servers or not self.mcp_config:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Build unique identifier for the tool
|
||||
tool_identifier = f"mcp__{server_name}__{tool_name}"
|
||||
|
||||
# Find corresponding tool in tool_list
|
||||
target_tool = None
|
||||
for tool in self.tool_list:
|
||||
if tool.get("type") == "function" and tool.get("function", {}).get("name") == tool_identifier:
|
||||
target_tool = tool
|
||||
break
|
||||
|
||||
if not target_tool:
|
||||
logging.warning(f"Tool not found: {tool_identifier}")
|
||||
return False
|
||||
|
||||
# Get tool parameter definitions
|
||||
function_info = target_tool.get("function", {})
|
||||
tool_parameters = function_info.get("parameters", {})
|
||||
properties = tool_parameters.get("properties", {})
|
||||
|
||||
# Check if session_id or task_id parameters are needed
|
||||
# Check if session_id is needed
|
||||
if "session_id" in properties:
|
||||
if hasattr(context, 'session_id') and context.session_id:
|
||||
parameter["session_id"] = context.session_id
|
||||
logging.info(f"Auto-added session_id: {context.session_id}")
|
||||
|
||||
# Check if task_id is needed
|
||||
if "task_id" in properties:
|
||||
if hasattr(context, 'task_id') and context.task_id:
|
||||
parameter["task_id"] = context.task_id
|
||||
logging.info(f"Auto-added task_id: {context.task_id}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logging.warning(f"Error checking tool parameters: {e}")
|
||||
return False
|
||||
|
||||
async def call_tool(
|
||||
self,
|
||||
action_list: List[Dict[str, Any]] = None,
|
||||
task_id: str = None,
|
||||
session_id: str = None,
|
||||
context: Context = None
|
||||
) -> List[ActionResult]:
|
||||
results = []
|
||||
if not action_list:
|
||||
return None
|
||||
|
||||
try:
|
||||
for action in action_list:
|
||||
if not isinstance(action, dict):
|
||||
action_dict = vars(action)
|
||||
else:
|
||||
action_dict = action
|
||||
|
||||
# Get values from dictionary
|
||||
server_name = action_dict.get("tool_name")
|
||||
tool_name = action_dict.get("action_name")
|
||||
parameter = action_dict.get("params", {})
|
||||
result_key = f"{server_name}__{tool_name}"
|
||||
|
||||
operation_info = {
|
||||
"server_name": server_name,
|
||||
"tool_name": tool_name,
|
||||
"params": parameter
|
||||
}
|
||||
|
||||
if not server_name or not tool_name:
|
||||
continue
|
||||
|
||||
# Check server type
|
||||
server_type = None
|
||||
if self.mcp_config and self.mcp_config.get("mcpServers"):
|
||||
server_config = self.mcp_config.get("mcpServers").get(server_name, {})
|
||||
server_type = server_config.get("type", "")
|
||||
|
||||
if server_type == "function_tool":
|
||||
try:
|
||||
call_result = await call_function_tool(
|
||||
server_name, tool_name, parameter, self.mcp_config
|
||||
)
|
||||
results.append(call_result)
|
||||
|
||||
self._update_metadata(result_key, call_result, operation_info)
|
||||
except Exception as e:
|
||||
logging.warning(f"Error calling function_tool tool: {e}")
|
||||
self._update_metadata(result_key, {"error": str(e)}, operation_info)
|
||||
continue
|
||||
|
||||
# For API type servers, use call_api function directly
|
||||
if server_type == "api":
|
||||
try:
|
||||
call_result = await call_api(
|
||||
server_name, tool_name, parameter, self.mcp_config
|
||||
)
|
||||
results.append(call_result)
|
||||
|
||||
self._update_metadata(result_key, call_result, operation_info)
|
||||
except Exception as e:
|
||||
logging.warning(f"Error calling API tool: {e}")
|
||||
self._update_metadata(result_key, {"error": str(e)}, operation_info)
|
||||
continue
|
||||
|
||||
# Prioritize using existing server instances
|
||||
server = self.server_instances.get(server_name)
|
||||
if server is None:
|
||||
# If it doesn't exist, create a new instance and save it
|
||||
server = await get_server_instance(server_name, self.mcp_config,context)
|
||||
if server:
|
||||
self.server_instances[server_name] = server
|
||||
logging.info(f"Created and cached new server instance for {server_name}")
|
||||
else:
|
||||
logging.warning(f"Created new server failed: {server_name}, session_id: {session_id}, tool_name: {tool_name}")
|
||||
|
||||
self._update_metadata(result_key, {"error": "Failed to create server instance"}, operation_info)
|
||||
continue
|
||||
|
||||
# Use server instance to call the tool
|
||||
call_result_raw = None
|
||||
action_result = ActionResult(
|
||||
tool_name=server_name,
|
||||
action_name=tool_name,
|
||||
content="",
|
||||
keep=True
|
||||
)
|
||||
max_retry = 3
|
||||
for i in range(max_retry):
|
||||
try:
|
||||
async def progress_callback(
|
||||
progress: float, total: float | None, message: str | None
|
||||
):
|
||||
try:
|
||||
output = Output()
|
||||
output.data = message
|
||||
tool_output_message = Message(
|
||||
category=Constants.OUTPUT,
|
||||
payload=output,
|
||||
sender=f"{server_name}__{tool_name}",
|
||||
session_id=context.session_id if context else "",
|
||||
headers={"context": context}
|
||||
)
|
||||
sync_exec(send_message, tool_output_message)
|
||||
except BaseException as e:
|
||||
logging.warning(f"Error calling progress callback: {e}")
|
||||
|
||||
await self.check_tool_params(context=context, server_name=server_name, tool_name=tool_name,
|
||||
parameter=parameter)
|
||||
call_result_raw = await server.call_tool(tool_name=tool_name, arguments=parameter,
|
||||
progress_callback=progress_callback)
|
||||
break
|
||||
except BaseException as e:
|
||||
logging.warning(
|
||||
f"Error calling tool error: {e}. Extra info: session_id = {session_id}, tool_name = {tool_name}."
|
||||
f"Traceback:\n{traceback.format_exc()}"
|
||||
)
|
||||
logging.info(f"tool_name:{server_name},action_name:{tool_name} finished.")
|
||||
logging.debug(f"tool_name:{server_name},action_name:{tool_name} call-mcp-tool-result: {call_result_raw}")
|
||||
if not call_result_raw:
|
||||
logging.warning(f"Error calling tool with cached server")
|
||||
|
||||
self._update_metadata(result_key, {"error": str(e)}, operation_info)
|
||||
|
||||
# If using cached server instance fails, try to clean up and recreate
|
||||
if server_name in self.server_instances:
|
||||
try:
|
||||
await cleanup_server(self.server_instances[server_name])
|
||||
del self.server_instances[server_name]
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to cleanup server {server_name}: {e}")
|
||||
else:
|
||||
if call_result_raw and call_result_raw.content:
|
||||
metadata = call_result_raw.content[0].model_extra.get("metadata", {})
|
||||
artifact_datas = []
|
||||
|
||||
content_list: list[str] = []
|
||||
for content in call_result_raw.content:
|
||||
if isinstance(call_result_raw.content[0], TextContent):
|
||||
content_list.append(content.text)
|
||||
_metadata = content.model_extra.get("metadata", {})
|
||||
if "artifact_data" in _metadata and isinstance(_metadata["artifact_data"], dict):
|
||||
artifact_datas.append({
|
||||
"artifact_type": _metadata["artifact_type"],
|
||||
"artifact_data": _metadata["artifact_data"]
|
||||
})
|
||||
elif isinstance(call_result_raw.content[0], ImageContent):
|
||||
content_list.append(f"data:image/jpeg;base64,{content.data}")
|
||||
_metadata = content.model_extra.get("metadata", {})
|
||||
if "artifact_data" in _metadata and isinstance(_metadata["artifact_data"], dict):
|
||||
artifact_datas.append({
|
||||
"artifact_type": _metadata["artifact_type"],
|
||||
"artifact_data": _metadata["artifact_data"]
|
||||
})
|
||||
if metadata and artifact_datas:
|
||||
metadata["artifacts"] = artifact_datas
|
||||
|
||||
action_result = ActionResult(
|
||||
tool_name=server_name,
|
||||
action_name=tool_name,
|
||||
content=json.dumps(content_list, ensure_ascii=False),
|
||||
keep=True,
|
||||
metadata=metadata,
|
||||
parameter=parameter
|
||||
)
|
||||
results.append(action_result)
|
||||
self._update_metadata(result_key, action_result, operation_info)
|
||||
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to call_tool: {e}.Extra info: session_id = {session_id}, action_list = {action_list}")
|
||||
return None
|
||||
|
||||
return results
|
||||
|
||||
def _update_metadata(self, result_key: str, result: Any, operation_info: Dict[str, Any]):
|
||||
"""
|
||||
Update sandbox metadata with a single tool call result
|
||||
|
||||
Args:
|
||||
result_key: The key name in metadata
|
||||
result: Tool call result
|
||||
operation_info: Operation information
|
||||
"""
|
||||
if not self.sandbox or not hasattr(self.sandbox, '_metadata'):
|
||||
return
|
||||
|
||||
try:
|
||||
metadata = self.sandbox._metadata.get("mcp_metadata", {})
|
||||
tmp_data = {
|
||||
"input": operation_info,
|
||||
"output": result
|
||||
}
|
||||
if not metadata:
|
||||
metadata["mcp_metadata"] = {}
|
||||
metadata["mcp_metadata"][result_key] = [tmp_data]
|
||||
self.sandbox._metadata["mcp_metadata"] = metadata
|
||||
return
|
||||
|
||||
_metadata = metadata.get(result_key, [])
|
||||
if not _metadata:
|
||||
_metadata[result_key] = [_metadata]
|
||||
else:
|
||||
_metadata[result_key].append(tmp_data)
|
||||
metadata[result_key] = _metadata
|
||||
self.sandbox._metadata["mcp_metadata"] = metadata
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
logging.debug(f"Failed to update sandbox metadata: {e}")
|
||||
|
||||
# Add cleanup method, called when Sandbox is destroyed
|
||||
async def cleanup(self):
|
||||
"""Clean up all server connections"""
|
||||
for server_name, server in list(self.server_instances.items()):
|
||||
try:
|
||||
await cleanup_server(server)
|
||||
del self.server_instances[server_name]
|
||||
logging.info(f"Cleaned up server instance for {server_name}")
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to cleanup server {server_name}: {e}")
|
||||
Reference in New Issue
Block a user