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

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
@@ -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}")