diff --git a/cookbook/zh/quickstart.md b/cookbook/zh/quickstart.md index 9a02c3c41..b354eed75 100644 --- a/cookbook/zh/quickstart.md +++ b/cookbook/zh/quickstart.md @@ -143,6 +143,8 @@ agent = AgnoAgent( print("✅ Agno agent created successfully") ``` +(autogen-agent-zh)= + #### (可选)使用AutoGen Agent ````{note} diff --git a/src/agentscope_runtime/sandbox/manager/container_clients/docker_client.py b/src/agentscope_runtime/sandbox/manager/container_clients/docker_client.py index aba81bee6..76bc0ee13 100644 --- a/src/agentscope_runtime/sandbox/manager/container_clients/docker_client.py +++ b/src/agentscope_runtime/sandbox/manager/container_clients/docker_client.py @@ -217,9 +217,12 @@ def _try_pull_from_acr(self, image): acr_registry = "agentscope-registry.ap-southeast-1.cr.aliyuncs.com" acr_image = f"{acr_registry}/{image}" - logger.debug(f"Attempting to pull from ACR: {acr_image}") + logger.info( + f"Attempting to pull from ACR: {acr_image}, it might take " + f"several minutes.", + ) self.client.images.pull(acr_image) - logger.debug(f"Successfully pulled image from ACR: {acr_image}") + logger.info(f"Successfully pulled image from ACR: {acr_image}") # Retag the image acr_img_obj = self.client.images.get(acr_image) @@ -265,11 +268,15 @@ def create( self.client.images.get(image) logger.debug(f"Image '{image}' found locally.") except docker.errors.ImageNotFound: - logger.debug( + logger.info( f"Image '{image}' not found locally. " f"Attempting to pull it...", ) try: + logger.info( + f"Attempting to pull: {image}, " + f"it might take several minutes.", + ) self.client.images.pull(image) logger.debug( f"Image '{image}' successfully pulled from default " @@ -280,7 +287,7 @@ def create( logger.warning( f"Failed to pull from default registry: {e}", ) - logger.debug("Trying to pull from ACR fallback...") + logger.warning("Trying to pull from ACR fallback...") pull_success = self._try_pull_from_acr(image) @@ -289,11 +296,11 @@ def create( f"Failed to pull image '{image}' from both " f"default and ACR", ) - return False + return None, None, None except docker.errors.APIError as e: logger.error(f"Error occurred while checking the image: {e}") - return False + return None, None, None # Create and run the container container = self.client.containers.run( @@ -307,10 +314,10 @@ def create( ) container.reload() _id = container.id - return _id, list(port_mapping.values()) + return _id, list(port_mapping.values()), "localhost" except Exception as e: logger.error(f"An error occurred: {e}, {traceback.format_exc()}") - return None, None + return None, None, None def start(self, container_id): """Start a Docker container.""" diff --git a/src/agentscope_runtime/sandbox/manager/container_clients/kubernetes_client.py b/src/agentscope_runtime/sandbox/manager/container_clients/kubernetes_client.py index 781790b72..0338d91c0 100644 --- a/src/agentscope_runtime/sandbox/manager/container_clients/kubernetes_client.py +++ b/src/agentscope_runtime/sandbox/manager/container_clients/kubernetes_client.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # pylint: disable=too-many-branches +import os import time import hashlib import traceback @@ -89,7 +90,9 @@ def _create_pod_spec( runtime_config = {} container_name = name or "main-container" + # Container specification + # TODO: use image from docker registry first container = client.V1Container( name=container_name, image=f"agentscope-registry.ap-southeast-1.cr.aliyuncs.com" @@ -244,6 +247,7 @@ def create( ) exposed_ports = [] + pod_node_ip = "localhost" # Auto-create services for exposed ports (like Docker's port # mapping) if ports: @@ -259,19 +263,22 @@ def create( parsed_ports, ) if service_created: - exposed_ports = self._get_service_node_ports(name) + ( + exposed_ports, + pod_node_ip, + ) = self._get_service_node_ports(name) logger.debug( f"Pod '{name}' created with exposed ports: {exposed_ports}", ) if not self.wait_for_pod_ready(name, timeout=60): logger.error(f"Pod '{name}' failed to become ready") - return None, None + return None, None, None - return name, exposed_ports + return name, exposed_ports, pod_node_ip except Exception as e: logger.error(f"An error occurred: {e}, {traceback.format_exc()}") - return None, None + return None, None, None def start(self, container_id): """ @@ -510,7 +517,10 @@ def _create_multi_port_service(self, pod_name, port_list): service = client.V1Service( api_version="v1", kind="Service", - metadata=client.V1ObjectMeta(name=service_name), + metadata=client.V1ObjectMeta( + name=service_name, + namespace=self.namespace, + ), spec=service_spec, ) @@ -540,11 +550,56 @@ def _get_service_node_ports(self, pod_name): ) node_ports = [] + pod_node_ip = self._get_pod_node_ip(pod_name) + for port in service_info.spec.ports: if port.node_port: node_ports.append(port.node_port) - return node_ports + return node_ports, pod_node_ip except Exception as e: logger.error(f"Failed to get node port: {e}") return None + + def _get_pod_node_ip(self, pod_name): + """Get the IP of the node where the pod is running""" + + # Check if we are running in Colima, where pod runs in VM + docker_host = os.getenv("DOCKER_HOST", "") + if "colima" in docker_host.lower(): + return "localhost" + + try: + pod = self.v1.read_namespaced_pod( + name=pod_name, + namespace=self.namespace, + ) + + node_name = pod.spec.node_name + if not node_name: + logger.warning( + f"Pod {pod_name} is not scheduled to any node yet", + ) + return None + + node = self.v1.read_node(name=node_name) + + external_ip = None + internal_ip = None + + for address in node.status.addresses: + if address.type == "ExternalIP": + external_ip = address.address + elif address.type == "InternalIP": + internal_ip = address.address + + result_ip = external_ip or internal_ip + logger.debug( + f"Using IP: {result_ip} (external: {external_ip}, internal:" + f" {internal_ip})", + ) + return result_ip + + except Exception as e: + logger.error(f"Failed to get pod node IP: {e}") + return None diff --git a/src/agentscope_runtime/sandbox/manager/sandbox_manager.py b/src/agentscope_runtime/sandbox/manager/sandbox_manager.py index 90e1051a5..4a087f239 100644 --- a/src/agentscope_runtime/sandbox/manager/sandbox_manager.py +++ b/src/agentscope_runtime/sandbox/manager/sandbox_manager.py @@ -9,6 +9,7 @@ from functools import wraps from typing import Optional, Dict +from urllib.parse import urlparse, urlunparse import shortuuid import requests @@ -16,7 +17,6 @@ from ..model import ( ContainerModel, SandboxManagerEnvConfig, - DEFAULT_LOCAL_MANAGER_CONFIG, ) from ..enums import SandboxType from ..registry import SandboxRegistry @@ -82,7 +82,7 @@ def wrapper(self, *args, **kwargs): class SandboxManager: def __init__( self, - config: SandboxManagerEnvConfig = DEFAULT_LOCAL_MANAGER_CONFIG, + config: Optional[SandboxManagerEnvConfig] = None, base_url=None, bearer_token=None, default_type: SandboxType | str = SandboxType.BASE, @@ -97,19 +97,28 @@ def __init__( self.http_session.headers.update( {"Authorization": f"Bearer {bearer_token}"}, ) + # Remote mode, return directly + return else: self.http_session = None self.base_url = None + if not config: + config = SandboxManagerEnvConfig( + file_system="local", + redis_enabled=False, + container_deployment="docker", + pool_size=0, + default_mount_dir="sessions_mount_dir", + ) + self.default_type = SandboxType(default_type) self.workdir = "/workspace" self.config = config self.pool_size = self.config.pool_size self.prefix = self.config.container_prefix_key - self.default_mount_dir = ( - self.config.default_mount_dir or "sessions_mount_dir" - ) + self.default_mount_dir = self.config.default_mount_dir self.storage_folder = ( self.config.storage_folder or self.default_mount_dir ) @@ -408,20 +417,24 @@ def create( short_uuid = shortuuid.ShortUUID(alphabet=alphabet).uuid() session_id = str(short_uuid) - if mount_dir is None: - mount_dir = os.path.join(self.default_mount_dir, session_id) - os.makedirs(mount_dir, exist_ok=True) + if not mount_dir: + if self.default_mount_dir: + mount_dir = os.path.join(self.default_mount_dir, session_id) + os.makedirs(mount_dir, exist_ok=True) - if not os.path.isabs(mount_dir): - mount_dir = os.path.abspath(mount_dir) + if mount_dir: + if not os.path.isabs(mount_dir): + mount_dir = os.path.abspath(mount_dir) if storage_path is None: - storage_path = self.storage.path_join( - self.storage_folder, - session_id, - ) + if self.storage_folder: + storage_path = self.storage.path_join( + self.storage_folder, + session_id, + ) - self.storage.download_folder(storage_path, mount_dir) + if mount_dir and storage_path: + self.storage.download_folder(storage_path, mount_dir) try: # Check for an existing container with the same name @@ -435,14 +448,17 @@ def create( runtime_token = secrets.token_hex(16) # Prepare volume bindings if a mount directory is provided - volume_bindings = { - mount_dir: { - "bind": self.workdir, - "mode": "rw", - }, - } + if mount_dir: + volume_bindings = { + mount_dir: { + "bind": self.workdir, + "mode": "rw", + }, + } + else: + volume_bindings = {} - _id, ports = self.client.create( + _id, ports, ip = self.client.create( image, name=container_name, ports=["80/tcp"], # Nginx @@ -471,16 +487,16 @@ def create( session_id=session_id, container_id=_id, container_name=container_name, - base_url=f"http://localhost:{ports[0]}/fastapi", - browser_url=f"http://localhost:{ports[0]}/steel-api" + base_url=f"http://{ip}:{ports[0]}/fastapi", + browser_url=f"http://{ip}:{ports[0]}/steel-api" f"/{runtime_token}", - front_browser_ws=f"ws://localhost:" + front_browser_ws=f"ws://{ip}:" f"{ports[0]}/steel-api/" f"{runtime_token}/v1/sessions/cast", - client_browser_ws=f"ws://localhost:" + client_browser_ws=f"ws://{ip}:" f"{ports[0]}/steel-api/{runtime_token}/&sessionId" f"={BROWSER_SESSION_ID}", - artifacts_sio=f"http://localhost:{ports[0]}/v1", + artifacts_sio=f"http://{ip}:{ports[0]}/v1", ports=[ports[0]], mount_dir=str(mount_dir), storage_path=storage_path, @@ -526,10 +542,11 @@ def release(self, identity): logger.debug(f"Container for {identity} destroyed.") # Upload to storage - self.storage.upload_folder( - container_info.mount_dir, - container_info.storage_path, - ) + if container_info.mount_dir and container_info.storage_path: + self.storage.upload_folder( + container_info.mount_dir, + container_info.storage_path, + ) return True except Exception as e: @@ -631,8 +648,20 @@ def _establish_connection(self, identity): "sandbox-appworld" in container_model.version or "sandbox-bfcl" in container_model.version ): + parsed = urlparse(container_model.base_url) + base_url = urlunparse( + ( + parsed.scheme, + parsed.netloc, + "", + "", + "", + "", + ), + ) + return TrainingSandboxClient( - base_url=f"http://localhost:{container_model.ports[0]}", + base_url=base_url, ).__enter__() return SandboxHttpClient( diff --git a/src/agentscope_runtime/sandbox/manager/server/app.py b/src/agentscope_runtime/sandbox/manager/server/app.py index ed658cbd3..56945df48 100644 --- a/src/agentscope_runtime/sandbox/manager/server/app.py +++ b/src/agentscope_runtime/sandbox/manager/server/app.py @@ -195,6 +195,33 @@ async def health_check(): ) +def setup_logging(log_level: str): + """Setup logging configuration based on log level""" + # Convert string to logging level + level_mapping = { + "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "WARNING": logging.WARNING, + "ERROR": logging.ERROR, + "CRITICAL": logging.CRITICAL, + } + + level = level_mapping.get(log_level.upper(), logging.INFO) + + # Reconfigure logging + logging.basicConfig( + level=level, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + force=True, # This will reconfigure existing loggers + ) + + # Update the logger for this module + global logger + logger.setLevel(level) + + logger.info(f"Logging level set to {log_level.upper()}") + + def main(): """Main entry point for the Runtime Manager Service""" import argparse @@ -203,8 +230,18 @@ def main(): parser = argparse.ArgumentParser(description="Runtime Manager Service") parser.add_argument("--config", type=str, help="Path to config file") + parser.add_argument( + "--log-level", + type=str, + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + default="INFO", + help="Set the logging level (default: INFO)", + ) args = parser.parse_args() + # Setup logging based on command line argument + setup_logging(args.log_level) + if args.config and not os.path.exists(args.config): raise FileNotFoundError( f"Error: Config file {args.config} does not exist", diff --git a/src/agentscope_runtime/sandbox/model/__init__.py b/src/agentscope_runtime/sandbox/model/__init__.py index 8c2c59fa8..25eb5f555 100644 --- a/src/agentscope_runtime/sandbox/model/__init__.py +++ b/src/agentscope_runtime/sandbox/model/__init__.py @@ -1,12 +1,8 @@ # -*- coding: utf-8 -*- from .container import ContainerModel -from .manager_config import ( - SandboxManagerEnvConfig, - DEFAULT_LOCAL_MANAGER_CONFIG, -) +from .manager_config import SandboxManagerEnvConfig __all__ = [ "ContainerModel", "SandboxManagerEnvConfig", - "DEFAULT_LOCAL_MANAGER_CONFIG", ] diff --git a/src/agentscope_runtime/sandbox/model/manager_config.py b/src/agentscope_runtime/sandbox/model/manager_config.py index e58ef45ef..cb30ca9a1 100644 --- a/src/agentscope_runtime/sandbox/model/manager_config.py +++ b/src/agentscope_runtime/sandbox/model/manager_config.py @@ -114,10 +114,8 @@ class SandboxManagerEnvConfig(BaseModel): @model_validator(mode="after") def check_settings(cls, self): - if not self.default_mount_dir: - raise ValueError("default_mount_dir must be set") - - os.makedirs(self.default_mount_dir, exist_ok=True) + if self.default_mount_dir: + os.makedirs(self.default_mount_dir, exist_ok=True) if self.file_system == "oss": required_oss_fields = [ @@ -164,12 +162,3 @@ def check_settings(cls, self): ) return self - - -DEFAULT_LOCAL_MANAGER_CONFIG = SandboxManagerEnvConfig( - file_system="local", - redis_enabled=False, - container_deployment="docker", - pool_size=0, - default_mount_dir="sessions_mount_dir", -)