diff --git a/app/agent/browser.py b/app/agent/browser.py index 5ddf345..9d20827 100644 --- a/app/agent/browser.py +++ b/app/agent/browser.py @@ -10,6 +10,7 @@ from app.schema import Message, ToolChoice from app.tool import BrowserUseTool, Terminate, ToolCollection from app.tool.sb_browser_tool import SandboxBrowserTool + # Avoid circular import if BrowserAgent needs BrowserContextHelper if TYPE_CHECKING: from app.agent.base import BaseAgent # Or wherever memory is defined @@ -23,7 +24,9 @@ class BrowserContextHelper: async def get_browser_state(self) -> Optional[dict]: browser_tool = self.agent.available_tools.get_tool(BrowserUseTool().name) if not browser_tool: - browser_tool = self.agent.available_tools.get_tool(SandboxBrowserTool().name) + browser_tool = self.agent.available_tools.get_tool( + SandboxBrowserTool().name + ) if not browser_tool or not hasattr(browser_tool, "get_current_state"): logger.warning("BrowserUseTool not found or doesn't have get_current_state") return None diff --git a/app/agent/manus.py b/app/agent/manus.py index 960ab08..df40edb 100644 --- a/app/agent/manus.py +++ b/app/agent/manus.py @@ -14,8 +14,6 @@ from app.tool.mcp import MCPClients, MCPClientTool from app.tool.python_execute import PythonExecute from app.tool.str_replace_editor import StrReplaceEditor -from app.tool.computer_use_tool import ComputerUseTool -from app.daytona.sandbox import create_sandbox class Manus(ToolCallAgent): """A versatile general-purpose agent with support for both local and MCP tools.""" diff --git a/app/agent/sandbox_agent.py b/app/agent/sandbox_agent.py index e1716b2..a510f68 100644 --- a/app/agent/sandbox_agent.py +++ b/app/agent/sandbox_agent.py @@ -1,36 +1,29 @@ -import json -from typing import TYPE_CHECKING, Dict, List, Optional +from typing import Dict, List, Optional -import daytona from pydantic import Field, model_validator from app.agent.browser import BrowserContextHelper from app.agent.toolcall import ToolCallAgent from app.config import config -from app.daytona.sandbox import create_sandbox, delete_sandbox, get_or_start_sandbox +from app.daytona.sandbox import create_sandbox, delete_sandbox from app.daytona.tool_base import SandboxToolsBase from app.logger import logger from app.prompt.manus import NEXT_STEP_PROMPT, SYSTEM_PROMPT -from app.schema import Message from app.tool import Terminate, ToolCollection from app.tool.ask_human import AskHuman from app.tool.browser_use_tool import BrowserUseTool from app.tool.mcp import MCPClients, MCPClientTool -from app.tool.python_execute import PythonExecute from app.tool.sb_browser_tool import SandboxBrowserTool from app.tool.sb_files_tool import SandboxFilesTool from app.tool.sb_shell_tool import SandboxShellTool from app.tool.sb_vision_tool import SandboxVisionTool -from app.tool.str_replace_editor import StrReplaceEditor class SandboxManus(ToolCallAgent): """A versatile general-purpose agent with support for both local and MCP tools.""" name: str = "SandboxManus" - description: str = ( - "A versatile agent that can solve various tasks using multiple sandbox-tools including MCP-based tools" - ) + description: str = "A versatile agent that can solve various tasks using multiple sandbox-tools including MCP-based tools" system_prompt: str = SYSTEM_PROMPT.format(directory=config.workspace_root) next_step_prompt: str = NEXT_STEP_PROMPT diff --git a/app/daytona/README.md b/app/daytona/README.md index 9a707b9..6f80206 100644 --- a/app/daytona/README.md +++ b/app/daytona/README.md @@ -55,4 +55,3 @@ ## Learn More - [Daytona Documentation](https://www.daytona.io/docs/) - diff --git a/app/daytona/api.py b/app/daytona/api.py index 8f90bb5..e1fdc16 100644 --- a/app/daytona/api.py +++ b/app/daytona/api.py @@ -2,23 +2,14 @@ import os import urllib.parse from typing import Optional -from fastapi import ( - FastAPI, - UploadFile, - File, - HTTPException, - APIRouter, - Form, - Depends, - Request, -) +from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile from fastapi.responses import Response from pydantic import BaseModel - -from sandbox.sandbox import get_or_start_sandbox, delete_sandbox -from utils.logger import logger -from utils.auth_utils import get_optional_user_id +from sandbox.sandbox import delete_sandbox, get_or_start_sandbox from services.supabase import DBConnection +from utils.auth_utils import get_optional_user_id +from utils.logger import logger + # Initialize shared resources router = APIRouter(tags=["sandbox"]) @@ -459,7 +450,7 @@ async def ensure_project_sandbox_active( # Get or start the sandbox logger.info(f"Ensuring sandbox is active for project {project_id}") - sandbox = await get_or_start_sandbox(sandbox_id) + await get_or_start_sandbox(sandbox_id) logger.info( f"Successfully ensured sandbox {sandbox_id} is active for project {project_id}" diff --git a/app/daytona/sandbox.py b/app/daytona/sandbox.py index a801a7f..6901a75 100644 --- a/app/daytona/sandbox.py +++ b/app/daytona/sandbox.py @@ -7,11 +7,11 @@ from daytona import ( SandboxState, SessionExecuteRequest, ) -from dotenv import load_dotenv from app.config import config from app.utils.logger import logger + # load_dotenv() daytona_settings = config.daytona logger.info("Initializing Daytona sandbox configuration") diff --git a/app/daytona/tool_base.py b/app/daytona/tool_base.py index 58fa629..043578a 100644 --- a/app/daytona/tool_base.py +++ b/app/daytona/tool_base.py @@ -1,4 +1,3 @@ -import asyncio from dataclasses import dataclass, field from datetime import datetime from typing import Any, ClassVar, Dict, Optional @@ -7,15 +6,12 @@ from daytona import Daytona, DaytonaConfig, Sandbox, SandboxState from pydantic import Field from app.config import config -from app.daytona.sandbox import ( - create_sandbox, - get_or_start_sandbox, - start_supervisord_session, -) -from app.tool.base import BaseTool, ToolResult +from app.daytona.sandbox import create_sandbox, start_supervisord_session +from app.tool.base import BaseTool from app.utils.files_utils import clean_path from app.utils.logger import logger + # load_dotenv() daytona_settings = config.daytona daytona_config = DaytonaConfig( diff --git a/app/tool/base.py b/app/tool/base.py index bcd9f00..fdb8b7d 100644 --- a/app/tool/base.py +++ b/app/tool/base.py @@ -1,10 +1,12 @@ -from abc import ABC, abstractmethod -from pydantic import BaseModel, Field -from typing import Any, Dict, Optional, Union -import inspect import json +from abc import ABC, abstractmethod +from typing import Any, Dict, Optional, Union + +from pydantic import BaseModel, Field + from app.utils.logger import logger + # class BaseTool(ABC, BaseModel): # name: str # description: str @@ -33,7 +35,6 @@ from app.utils.logger import logger # } - class ToolResult(BaseModel): """Represents the result of a tool execution.""" @@ -73,6 +74,7 @@ class ToolResult(BaseModel): # return self.copy(update=kwargs) return type(self)(**{**self.dict(), **kwargs}) + class BaseTool(ABC, BaseModel): """Consolidated base class for all tools combining BaseModel and Tool functionality. @@ -169,6 +171,8 @@ class BaseTool(ABC, BaseModel): """ logger.debug(f"Tool {self.__class__.__name__} returned failed result: {msg}") return ToolResult(error=msg) + + class CLIResult(ToolResult): """A ToolResult that can be rendered as a CLI output.""" diff --git a/app/tool/computer_use_tool.py b/app/tool/computer_use_tool.py index fd979e2..0ea57a7 100644 --- a/app/tool/computer_use_tool.py +++ b/app/tool/computer_use_tool.py @@ -1,26 +1,89 @@ +import asyncio +import base64 +import logging import os import time -import base64 +from typing import Dict, Literal, Optional + import aiohttp -import asyncio -import logging -from typing import Optional, Dict,Literal -import os from pydantic import Field -from app.daytona.tool_base import SandboxToolsBase, Sandbox + +from app.daytona.tool_base import Sandbox, SandboxToolsBase from app.tool.base import ToolResult KEYBOARD_KEYS = [ - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', - 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'enter', 'esc', 'backspace', 'tab', 'space', 'delete', - 'ctrl', 'alt', 'shift', 'win', - 'up', 'down', 'left', 'right', - 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12', - 'ctrl+c', 'ctrl+v', 'ctrl+x', 'ctrl+z', 'ctrl+a', 'ctrl+s', - 'alt+tab', 'alt+f4', 'ctrl+alt+delete' + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "enter", + "esc", + "backspace", + "tab", + "space", + "delete", + "ctrl", + "alt", + "shift", + "win", + "up", + "down", + "left", + "right", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "f10", + "f11", + "f12", + "ctrl+c", + "ctrl+v", + "ctrl+x", + "ctrl+z", + "ctrl+a", + "ctrl+s", + "alt+tab", + "alt+f4", + "ctrl+alt+delete", ] MOUSE_BUTTONS = ["left", "right", "middle"] _COMPUTER_USE_DESCRIPTION = """\ @@ -34,8 +97,11 @@ Key capabilities include: * Screenshots: Capture and save screen images * Waiting: Pause execution for specified duration """ + + class ComputerUseTool(SandboxToolsBase): """Computer automation tool for controlling the desktop environment.""" + name: str = "computer_use" description: str = _COMPUTER_USE_DESCRIPTION parameters: dict = { @@ -54,55 +120,46 @@ class ComputerUseTool(SandboxToolsBase): "mouse_up", "drag_to", "hotkey", - "screenshot" + "screenshot", ], "description": "The computer action to perform", }, - "x": { - "type": "number", - "description": "X coordinate for mouse actions" - }, - "y": { - "type": "number", - "description": "Y coordinate for mouse actions" - }, + "x": {"type": "number", "description": "X coordinate for mouse actions"}, + "y": {"type": "number", "description": "Y coordinate for mouse actions"}, "button": { "type": "string", "enum": MOUSE_BUTTONS, "description": "Mouse button for click/drag actions", - "default": "left" + "default": "left", }, "num_clicks": { "type": "integer", "description": "Number of clicks", "enum": [1, 2, 3], - "default": 1 + "default": 1, }, "amount": { "type": "integer", "description": "Scroll amount (positive for up, negative for down)", "minimum": -10, - "maximum": 10 - }, - "text": { - "type": "string", - "description": "Text to type" + "maximum": 10, }, + "text": {"type": "string", "description": "Text to type"}, "key": { "type": "string", "enum": KEYBOARD_KEYS, - "description": "Key to press" + "description": "Key to press", }, "keys": { "type": "string", "enum": KEYBOARD_KEYS, - "description": "Key combination to press" + "description": "Key combination to press", }, "duration": { "type": "number", "description": "Duration in seconds to wait", - "default": 0.5 - } + "default": 0.5, + }, }, "required": ["action"], "dependencies": { @@ -116,20 +173,23 @@ class ComputerUseTool(SandboxToolsBase): "mouse_up": [], "drag_to": ["x", "y"], "hotkey": ["keys"], - "screenshot": [] - } + "screenshot": [], + }, } session: Optional[aiohttp.ClientSession] = Field(default=None, exclude=True) mouse_x: int = Field(default=0, exclude=True) mouse_y: int = Field(default=0, exclude=True) api_base_url: Optional[str] = Field(default=None, exclude=True) + def __init__(self, sandbox: Optional[Sandbox] = None, **data): """Initialize with optional sandbox.""" super().__init__(**data) if sandbox is not None: self._sandbox = sandbox # 直接操作基类的私有属性 self.api_base_url = sandbox.get_preview_link(8000).url - logging.info(f"Initialized ComputerUseTool with API URL: {self.api_base_url}") + logging.info( + f"Initialized ComputerUseTool with API URL: {self.api_base_url}" + ) @classmethod def create_with_sandbox(cls, sandbox: Sandbox) -> "ComputerUseTool": @@ -141,7 +201,10 @@ class ComputerUseTool(SandboxToolsBase): if self.session is None or self.session.closed: self.session = aiohttp.ClientSession() return self.session - async def _api_request(self, method: str, endpoint: str, data: Optional[Dict] = None) -> Dict: + + async def _api_request( + self, method: str, endpoint: str, data: Optional[Dict] = None + ) -> Dict: """Send request to automation service API.""" try: session = await self._get_session() @@ -158,11 +221,21 @@ class ComputerUseTool(SandboxToolsBase): except Exception as e: logging.error(f"API request failed: {str(e)}") return {"success": False, "error": str(e)} + async def execute( self, action: Literal[ - "move_to", "click", "scroll", "typing", "press", - "wait", "mouse_down", "mouse_up", "drag_to", "hotkey", "screenshot" + "move_to", + "click", + "scroll", + "typing", + "press", + "wait", + "mouse_down", + "mouse_up", + "drag_to", + "hotkey", + "screenshot", ], x: Optional[float] = None, y: Optional[float] = None, @@ -173,7 +246,7 @@ class ComputerUseTool(SandboxToolsBase): key: Optional[str] = None, keys: Optional[str] = None, duration: float = 0.5, - **kwargs + **kwargs, ) -> ToolResult: """ Execute a specified computer automation action. @@ -198,74 +271,91 @@ class ComputerUseTool(SandboxToolsBase): return ToolResult(error="x and y coordinates are required") x_int = int(round(float(x))) y_int = int(round(float(y))) - result = await self._api_request("POST", "/automation/mouse/move", { - "x": x_int, - "y": y_int - }) + result = await self._api_request( + "POST", "/automation/mouse/move", {"x": x_int, "y": y_int} + ) if result.get("success", False): self.mouse_x = x_int self.mouse_y = y_int return ToolResult(output=f"Moved to ({x_int}, {y_int})") else: - return ToolResult(error=f"Failed to move: {result.get('error', 'Unknown error')}") + return ToolResult( + error=f"Failed to move: {result.get('error', 'Unknown error')}" + ) elif action == "click": x_val = x if x is not None else self.mouse_x y_val = y if y is not None else self.mouse_y x_int = int(round(float(x_val))) y_int = int(round(float(y_val))) num_clicks = int(num_clicks) - result = await self._api_request("POST", "/automation/mouse/click", { - "x": x_int, - "y": y_int, - "clicks": num_clicks, - "button": button.lower() - }) + result = await self._api_request( + "POST", + "/automation/mouse/click", + { + "x": x_int, + "y": y_int, + "clicks": num_clicks, + "button": button.lower(), + }, + ) if result.get("success", False): self.mouse_x = x_int self.mouse_y = y_int - return ToolResult(output=f"{num_clicks} {button} click(s) performed at ({x_int}, {y_int})") + return ToolResult( + output=f"{num_clicks} {button} click(s) performed at ({x_int}, {y_int})" + ) else: - return ToolResult(error=f"Failed to click: {result.get('error', 'Unknown error')}") + return ToolResult( + error=f"Failed to click: {result.get('error', 'Unknown error')}" + ) elif action == "scroll": if amount is None: return ToolResult(error="Scroll amount is required") amount = int(float(amount)) amount = max(-10, min(10, amount)) - result = await self._api_request("POST", "/automation/mouse/scroll", { - "clicks": amount, - "x": self.mouse_x, - "y": self.mouse_y - }) + result = await self._api_request( + "POST", + "/automation/mouse/scroll", + {"clicks": amount, "x": self.mouse_x, "y": self.mouse_y}, + ) if result.get("success", False): direction = "up" if amount > 0 else "down" steps = abs(amount) - return ToolResult(output=f"Scrolled {direction} {steps} step(s) at position ({self.mouse_x}, {self.mouse_y})") + return ToolResult( + output=f"Scrolled {direction} {steps} step(s) at position ({self.mouse_x}, {self.mouse_y})" + ) else: - return ToolResult(error=f"Failed to scroll: {result.get('error', 'Unknown error')}") + return ToolResult( + error=f"Failed to scroll: {result.get('error', 'Unknown error')}" + ) elif action == "typing": if text is None: return ToolResult(error="Text is required for typing") text = str(text) - result = await self._api_request("POST", "/automation/keyboard/write", { - "message": text, - "interval": 0.01 - }) + result = await self._api_request( + "POST", + "/automation/keyboard/write", + {"message": text, "interval": 0.01}, + ) if result.get("success", False): return ToolResult(output=f"Typed: {text}") else: - return ToolResult(error=f"Failed to type: {result.get('error', 'Unknown error')}") + return ToolResult( + error=f"Failed to type: {result.get('error', 'Unknown error')}" + ) elif action == "press": if key is None: return ToolResult(error="Key is required for press action") key = str(key).lower() - result = await self._api_request("POST", "/automation/keyboard/press", { - "keys": key, - "presses": 1 - }) + result = await self._api_request( + "POST", "/automation/keyboard/press", {"keys": key, "presses": 1} + ) if result.get("success", False): return ToolResult(output=f"Pressed key: {key}") else: - return ToolResult(error=f"Failed to press key: {result.get('error', 'Unknown error')}") + return ToolResult( + error=f"Failed to press key: {result.get('error', 'Unknown error')}" + ) elif action == "wait": duration = float(duration) duration = max(0, min(10, duration)) @@ -276,33 +366,41 @@ class ComputerUseTool(SandboxToolsBase): y_val = y if y is not None else self.mouse_y x_int = int(round(float(x_val))) y_int = int(round(float(y_val))) - result = await self._api_request("POST", "/automation/mouse/down", { - "x": x_int, - "y": y_int, - "button": button.lower() - }) + result = await self._api_request( + "POST", + "/automation/mouse/down", + {"x": x_int, "y": y_int, "button": button.lower()}, + ) if result.get("success", False): self.mouse_x = x_int self.mouse_y = y_int - return ToolResult(output=f"{button} button pressed at ({x_int}, {y_int})") + return ToolResult( + output=f"{button} button pressed at ({x_int}, {y_int})" + ) else: - return ToolResult(error=f"Failed to press button: {result.get('error', 'Unknown error')}") + return ToolResult( + error=f"Failed to press button: {result.get('error', 'Unknown error')}" + ) elif action == "mouse_up": x_val = x if x is not None else self.mouse_x y_val = y if y is not None else self.mouse_y x_int = int(round(float(x_val))) y_int = int(round(float(y_val))) - result = await self._api_request("POST", "/automation/mouse/up", { - "x": x_int, - "y": y_int, - "button": button.lower() - }) + result = await self._api_request( + "POST", + "/automation/mouse/up", + {"x": x_int, "y": y_int, "button": button.lower()}, + ) if result.get("success", False): self.mouse_x = x_int self.mouse_y = y_int - return ToolResult(output=f"{button} button released at ({x_int}, {y_int})") + return ToolResult( + output=f"{button} button released at ({x_int}, {y_int})" + ) else: - return ToolResult(error=f"Failed to release button: {result.get('error', 'Unknown error')}") + return ToolResult( + error=f"Failed to release button: {result.get('error', 'Unknown error')}" + ) elif action == "drag_to": if x is None or y is None: return ToolResult(error="x and y coordinates are required") @@ -310,31 +408,37 @@ class ComputerUseTool(SandboxToolsBase): target_y = int(round(float(y))) start_x = self.mouse_x start_y = self.mouse_y - result = await self._api_request("POST", "/automation/mouse/drag", { - "x": target_x, - "y": target_y, - "duration": 0.3, - "button": "left" - }) + result = await self._api_request( + "POST", + "/automation/mouse/drag", + {"x": target_x, "y": target_y, "duration": 0.3, "button": "left"}, + ) if result.get("success", False): self.mouse_x = target_x self.mouse_y = target_y - return ToolResult(output=f"Dragged from ({start_x}, {start_y}) to ({target_x}, {target_y})") + return ToolResult( + output=f"Dragged from ({start_x}, {start_y}) to ({target_x}, {target_y})" + ) else: - return ToolResult(error=f"Failed to drag: {result.get('error', 'Unknown error')}") + return ToolResult( + error=f"Failed to drag: {result.get('error', 'Unknown error')}" + ) elif action == "hotkey": if keys is None: return ToolResult(error="Keys are required for hotkey action") keys = str(keys).lower().strip() - key_sequence = keys.split('+') - result = await self._api_request("POST", "/automation/keyboard/hotkey", { - "keys": key_sequence, - "interval": 0.01 - }) + key_sequence = keys.split("+") + result = await self._api_request( + "POST", + "/automation/keyboard/hotkey", + {"keys": key_sequence, "interval": 0.01}, + ) if result.get("success", False): return ToolResult(output=f"Pressed key combination: {keys}") else: - return ToolResult(error=f"Failed to press keys: {result.get('error', 'Unknown error')}") + return ToolResult( + error=f"Failed to press keys: {result.get('error', 'Unknown error')}" + ) elif action == "screenshot": result = await self._api_request("POST", "/automation/screenshot") if "image" in result: @@ -344,18 +448,20 @@ class ComputerUseTool(SandboxToolsBase): screenshots_dir = "screenshots" if not os.path.exists(screenshots_dir): os.makedirs(screenshots_dir) - timestamped_filename = os.path.join(screenshots_dir, f"screenshot_{timestamp}.png") + timestamped_filename = os.path.join( + screenshots_dir, f"screenshot_{timestamp}.png" + ) latest_filename = "latest_screenshot.png" # Decode base64 string and save to file img_data = base64.b64decode(base64_str) - with open(timestamped_filename, 'wb') as f: + with open(timestamped_filename, "wb") as f: f.write(img_data) # Save a copy as the latest screenshot - with open(latest_filename, 'wb') as f: + with open(latest_filename, "wb") as f: f.write(img_data) return ToolResult( output=f"Screenshot saved as {timestamped_filename}", - base64_image=base64_str + base64_image=base64_str, ) else: return ToolResult(error="Failed to capture screenshot") @@ -363,6 +469,7 @@ class ComputerUseTool(SandboxToolsBase): return ToolResult(error=f"Unknown action: {action}") except Exception as e: return ToolResult(error=f"Computer action failed: {str(e)}") + async def cleanup(self): """Clean up resources.""" if self.session and not self.session.closed: @@ -371,7 +478,7 @@ class ComputerUseTool(SandboxToolsBase): def __del__(self): """Ensure cleanup on destruction.""" - if hasattr(self, 'session') and self.session is not None: + if hasattr(self, "session") and self.session is not None: try: asyncio.run(self.cleanup()) except RuntimeError: diff --git a/app/tool/sb_browser_tool.py b/app/tool/sb_browser_tool.py index 01cffa3..3725cbf 100644 --- a/app/tool/sb_browser_tool.py +++ b/app/tool/sb_browser_tool.py @@ -15,6 +15,7 @@ from app.daytona.tool_base import ( # Ensure Sandbox is imported correctly from app.tool.base import ToolResult from app.utils.logger import logger + # Context = TypeVar("Context") _BROWSER_DESCRIPTION = """\ A sandbox-based browser automation tool that allows interaction with web pages through various actions. diff --git a/app/tool/sb_files_tool.py b/app/tool/sb_files_tool.py index 405b5cd..be558b0 100644 --- a/app/tool/sb_files_tool.py +++ b/app/tool/sb_files_tool.py @@ -1,10 +1,13 @@ -from pydantic import Field -from typing import Optional, TypeVar -from app.tool.base import ToolResult -from app.daytona.tool_base import SandboxToolsBase, Sandbox -from app.utils.files_utils import should_exclude_file, clean_path -from app.utils.logger import logger import asyncio +from typing import Optional, TypeVar + +from pydantic import Field + +from app.daytona.tool_base import Sandbox, SandboxToolsBase +from app.tool.base import ToolResult +from app.utils.files_utils import clean_path, should_exclude_file +from app.utils.logger import logger + Context = TypeVar("Context") @@ -349,7 +352,6 @@ class SandboxFilesTool(SandboxToolsBase): async def cleanup(self): """Clean up sandbox resources.""" - pass @classmethod def create_with_context(cls, context: Context) -> "SandboxFilesTool[Context]": diff --git a/app/tool/sb_shell_tool.py b/app/tool/sb_shell_tool.py index cfd3a85..8a45244 100644 --- a/app/tool/sb_shell_tool.py +++ b/app/tool/sb_shell_tool.py @@ -1,17 +1,19 @@ import asyncio -from typing import Optional, Dict, Any, TypeVar import time +from typing import Any, Dict, Optional, TypeVar from uuid import uuid4 + +from app.daytona.tool_base import Sandbox, SandboxToolsBase from app.tool.base import ToolResult -from app.daytona.tool_base import SandboxToolsBase, Sandbox from app.utils.logger import logger + Context = TypeVar("Context") _SHELL_DESCRIPTION = """\ -Execute a shell command in the workspace directory. -IMPORTANT: Commands are non-blocking by default and run in a tmux session. -This is ideal for long-running operations like starting servers or build processes. -Uses sessions to maintain state between commands. +Execute a shell command in the workspace directory. +IMPORTANT: Commands are non-blocking by default and run in a tmux session. +This is ideal for long-running operations like starting servers or build processes. +Uses sessions to maintain state between commands. This tool is essential for running CLI tools, installing packages, and managing system operations. """ @@ -415,4 +417,3 @@ class SandboxShellTool(SandboxToolsBase): await self._execute_raw_command("tmux kill-server 2>/dev/null || true") except Exception as e: logger.error(f"Error shell box cleanup action: {e}") - pass diff --git a/app/tool/sb_vision_tool.py b/app/tool/sb_vision_tool.py index e550626..7559450 100644 --- a/app/tool/sb_vision_tool.py +++ b/app/tool/sb_vision_tool.py @@ -1,13 +1,15 @@ -from pydantic import Field -import os import base64 import mimetypes -from typing import Optional +import os from io import BytesIO -from PIL import Image +from typing import Optional +from PIL import Image +from pydantic import Field + +from app.daytona.tool_base import Sandbox, SandboxToolsBase, ThreadMessage from app.tool.base import ToolResult -from app.daytona.tool_base import SandboxToolsBase, Sandbox, ThreadMessage + # 最大文件大小(原图10MB,压缩后5MB) MAX_IMAGE_SIZE = 10 * 1024 * 1024 @@ -99,7 +101,7 @@ class SandboxVisionTool(SandboxToolsBase): output_mime = "image/jpeg" compressed_bytes = output.getvalue() return compressed_bytes, output_mime - except Exception as e: + except Exception: return image_bytes, mime_type async def execute( @@ -122,9 +124,7 @@ class SandboxVisionTool(SandboxToolsBase): try: file_info = self.sandbox.fs.get_file_info(full_path) if file_info.is_dir: - return self.fail_response( - f"路径 '{cleaned_path}' 是目录,不是图片文件。" - ) + return self.fail_response(f"路径 '{cleaned_path}' 是目录,不是图片文件。") except Exception: return self.fail_response(f"图片文件未找到: '{cleaned_path}'") if file_info.size > MAX_IMAGE_SIZE: diff --git a/app/utils/__init__.py b/app/utils/__init__.py index 8ab9008..4d3ecf1 100644 --- a/app/utils/__init__.py +++ b/app/utils/__init__.py @@ -1 +1 @@ -# Utility functions and constants for agent tools \ No newline at end of file +# Utility functions and constants for agent tools diff --git a/app/utils/files_utils.py b/app/utils/files_utils.py index fb3f567..d14ad55 100644 --- a/app/utils/files_utils.py +++ b/app/utils/files_utils.py @@ -1,6 +1,6 @@ - import os + # Files to exclude from operations EXCLUDED_FILES = { ".DS_Store", @@ -15,13 +15,7 @@ EXCLUDED_FILES = { } # Directories to exclude from operations -EXCLUDED_DIRS = { - "node_modules", - ".next", - "dist", - "build", - ".git" -} +EXCLUDED_DIRS = {"node_modules", ".next", "dist", "build", ".git"} # File extensions to exclude from operations EXCLUDED_EXT = { @@ -35,9 +29,10 @@ EXCLUDED_EXT = { ".tiff", ".webp", ".db", - ".sql" + ".sql", } + def should_exclude_file(rel_path: str) -> bool: """Check if a file should be excluded based on path, name, or extension @@ -64,6 +59,7 @@ def should_exclude_file(rel_path: str) -> bool: return False + def clean_path(path: str, workspace_path: str = "/workspace") -> str: """Clean and normalize a path to be relative to the workspace @@ -75,17 +71,17 @@ def clean_path(path: str, workspace_path: str = "/workspace") -> str: The cleaned path, relative to the workspace """ # Remove any leading slash - path = path.lstrip('/') + path = path.lstrip("/") # Remove workspace prefix if present - if path.startswith(workspace_path.lstrip('/')): - path = path[len(workspace_path.lstrip('/')):] + if path.startswith(workspace_path.lstrip("/")): + path = path[len(workspace_path.lstrip("/")) :] # Remove workspace/ prefix if present - if path.startswith('workspace/'): + if path.startswith("workspace/"): path = path[9:] # Remove any remaining leading slash - path = path.lstrip('/') + path = path.lstrip("/") return path diff --git a/app/utils/logger.py b/app/utils/logger.py index 6b36fa4..3c4f645 100644 --- a/app/utils/logger.py +++ b/app/utils/logger.py @@ -1,4 +1,8 @@ -import structlog, logging, os +import logging +import os + +import structlog + ENV_MODE = os.getenv("ENV_MODE", "LOCAL") diff --git a/protocol/a2a/app/README.md b/protocol/a2a/app/README.md index c5ee04a..6b413df 100644 --- a/protocol/a2a/app/README.md +++ b/protocol/a2a/app/README.md @@ -192,4 +192,3 @@ Response: ## Learn More - [A2A Protocol Documentation](https://google.github.io/A2A/#/documentation) - diff --git a/protocol/a2a/app/README_zh.md b/protocol/a2a/app/README_zh.md index afa6fac..f97645a 100644 --- a/protocol/a2a/app/README_zh.md +++ b/protocol/a2a/app/README_zh.md @@ -192,4 +192,3 @@ Response: ## Learn More - [A2A Protocol Documentation](https://google.github.io/A2A/#/documentation) - diff --git a/protocol/a2a/app/agent.py b/protocol/a2a/app/agent.py index 9559ceb..64ba414 100644 --- a/protocol/a2a/app/agent.py +++ b/protocol/a2a/app/agent.py @@ -1,6 +1,7 @@ -import httpx -from typing import Any, Dict, AsyncIterable, Literal, List, ClassVar +from typing import Any, AsyncIterable, ClassVar, Dict, List, Literal + from pydantic import BaseModel + from app.agent.manus import Manus @@ -12,7 +13,6 @@ class ResponseFormat(BaseModel): class A2AManus(Manus): - async def invoke(self, query, sessionId) -> str: config = {"configurable": {"thread_id": sessionId}} response = await self.run(query) diff --git a/protocol/a2a/app/agent_executor.py b/protocol/a2a/app/agent_executor.py index bce0b1d..3b44261 100644 --- a/protocol/a2a/app/agent_executor.py +++ b/protocol/a2a/app/agent_executor.py @@ -1,8 +1,8 @@ import logging +from typing import Awaitable, Callable from a2a.server.agent_execution import AgentExecutor, RequestContext -from a2a.server.events import Event, EventQueue -from a2a.server.tasks import TaskUpdater +from a2a.server.events import EventQueue from a2a.types import ( InvalidParamsError, Part, @@ -10,13 +10,11 @@ from a2a.types import ( TextPart, UnsupportedOperationError, ) -from a2a.utils import ( - completed_task, - new_artifact, -) -from .agent import A2AManus +from a2a.utils import completed_task, new_artifact from a2a.utils.errors import ServerError -from typing import Callable, Awaitable + +from .agent import A2AManus + logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) diff --git a/protocol/a2a/app/main.py b/protocol/a2a/app/main.py index dd09f23..e69a12b 100644 --- a/protocol/a2a/app/main.py +++ b/protocol/a2a/app/main.py @@ -1,25 +1,22 @@ -import httpx import argparse +import asyncio +import logging +from typing import Optional +import httpx from a2a.server.apps import A2AStarletteApplication from a2a.server.request_handlers import DefaultRequestHandler -from a2a.server.tasks import InMemoryTaskStore, InMemoryPushNotifier -from a2a.types import ( - AgentCapabilities, - AgentCard, - AgentSkill, -) +from a2a.server.tasks import InMemoryPushNotifier, InMemoryTaskStore +from a2a.types import AgentCapabilities, AgentCard, AgentSkill +from dotenv import load_dotenv -from .agent_executor import ManusExecutor - -from .agent import A2AManus from app.tool.browser_use_tool import _BROWSER_DESCRIPTION from app.tool.str_replace_editor import _STR_REPLACE_EDITOR_DESCRIPTION from app.tool.terminate import _TERMINATE_DESCRIPTION -import logging -from dotenv import load_dotenv -import asyncio -from typing import Optional + +from .agent import A2AManus +from .agent_executor import ManusExecutor + load_dotenv() diff --git a/tests/daytona/test_computer_use_tool.py b/tests/daytona/test_computer_use_tool.py index 545dcb1..0b359e5 100644 --- a/tests/daytona/test_computer_use_tool.py +++ b/tests/daytona/test_computer_use_tool.py @@ -1,11 +1,13 @@ -from app.tool.computer_use_tool import ComputerUseTool -from app.daytona.sandbox import create_sandbox,start_supervisord_session import asyncio +from app.daytona.sandbox import create_sandbox +from app.tool.computer_use_tool import ComputerUseTool + + async def main(): # 创建沙箱和工具 sandbox = create_sandbox(password="123456") - base_url=sandbox.get_preview_link(8000) + base_url = sandbox.get_preview_link(8000) print(f"Sandbox base URL: {base_url}") print(f"Sandbox ID: {sandbox.id}") @@ -18,5 +20,6 @@ async def main(): # 清理资源(可选) await computer_tool.cleanup() + if __name__ == "__main__": asyncio.run(main()) # 运行异步主函数 diff --git a/tests/daytona/test_daytona.py b/tests/daytona/test_daytona.py index ddacad4..692f740 100644 --- a/tests/daytona/test_daytona.py +++ b/tests/daytona/test_daytona.py @@ -1,10 +1,5 @@ -from daytona import ( - CreateSandboxFromImageParams, - Daytona, - DaytonaConfig, - Image, - Resources, -) +from daytona import CreateSandboxFromImageParams, Daytona, DaytonaConfig, Resources + # Using environment variables (DAYTONA_API_KEY, DAYTONA_API_URL, DAYTONA_TARGET) # daytona = Daytona() diff --git a/tests/daytona/test_sb_browser_use.py b/tests/daytona/test_sb_browser_use.py index 3b210f7..af28980 100644 --- a/tests/daytona/test_sb_browser_use.py +++ b/tests/daytona/test_sb_browser_use.py @@ -1,11 +1,7 @@ import asyncio -import json -from daytona import Daytona, DaytonaConfig - -from app.daytona.sandbox import create_sandbox, start_supervisord_session +from app.daytona.sandbox import create_sandbox from app.tool.sb_browser_tool import SandboxBrowserTool -from app.utils.logger import logger async def main(): diff --git a/tests/daytona/test_sb_files_tool.py b/tests/daytona/test_sb_files_tool.py index ccf2ed2..d507658 100644 --- a/tests/daytona/test_sb_files_tool.py +++ b/tests/daytona/test_sb_files_tool.py @@ -1,7 +1,9 @@ -from app.tool.sb_files_tool import SandboxFilesTool -from app.daytona.sandbox import create_sandbox import asyncio +from app.daytona.sandbox import create_sandbox +from app.tool.sb_files_tool import SandboxFilesTool + + async def main(): # 创建沙箱和工具 sandbox = create_sandbox(password="123456") @@ -17,15 +19,18 @@ async def main(): sb_files_tool = SandboxFilesTool(sandbox) - # 执行创建文件操作 - result = await sb_files_tool.execute(action="create_file", file_path="src/a.txt", file_contents="aaaaa1111") + result = await sb_files_tool.execute( + action="create_file", file_path="src/a.txt", file_contents="aaaaa1111" + ) print(result) # 执行字符串替换操作 # result = await sb_files_tool.execute(action="str_replace", file_path="src/a.txt", old_str="aaaaa", new_str="bbbbb") # print(result) # 执行全文件重写操作 - result = await sb_files_tool.execute(action="full_file_rewrite", file_path="src/a.txt", file_contents="1234567") + result = await sb_files_tool.execute( + action="full_file_rewrite", file_path="src/a.txt", file_contents="1234567" + ) print(result) # 执行删除文件操作 # result = await sb_files_tool.execute(action="delete_file", file_path="src/a.txt") @@ -33,5 +38,6 @@ async def main(): # 清理资源(可选) # await sb_shell_tool.cleanup() + if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/tests/daytona/test_sb_shell_tools.py b/tests/daytona/test_sb_shell_tools.py index 25762a1..8c69f51 100644 --- a/tests/daytona/test_sb_shell_tools.py +++ b/tests/daytona/test_sb_shell_tools.py @@ -1,7 +1,9 @@ -from app.tool.sb_shell_tool import SandboxShellTool -from app.daytona.sandbox import create_sandbox import asyncio +from app.daytona.sandbox import create_sandbox +from app.tool.sb_shell_tool import SandboxShellTool + + async def main(): # 创建沙箱和工具 sandbox = create_sandbox(password="123456") @@ -16,7 +18,6 @@ async def main(): print(f"Website Link: {website_link}") sb_shell_tool = SandboxShellTool(sandbox) - # 执行截图操作 result = await sb_shell_tool.execute(action="execute_command", command="pwd") print(result) @@ -24,6 +25,7 @@ async def main(): # 清理资源(可选) # await sb_shell_tool.cleanup() + if __name__ == "__main__": print("123") - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/tests/daytona/test_sb_vision_tool.py b/tests/daytona/test_sb_vision_tool.py index 9915125..c06a51c 100644 --- a/tests/daytona/test_sb_vision_tool.py +++ b/tests/daytona/test_sb_vision_tool.py @@ -1,8 +1,8 @@ -from app.tool.sb_shell_tool import SandboxShellTool -from app.tool.sb_vision_tool import SandboxVisionTool -from app.daytona.sandbox import create_sandbox import asyncio +from app.daytona.sandbox import create_sandbox +from app.tool.sb_shell_tool import SandboxShellTool +from app.tool.sb_vision_tool import SandboxVisionTool async def main(): @@ -21,14 +21,20 @@ async def main(): sb_vision_tool = SandboxVisionTool(sandbox) # 执行终端命令 - result = await sb_shell_tool.execute(action="execute_command", command="curl -O http://img.netbian.com/file/2025/0716/091412RIFD9.jpg") + result = await sb_shell_tool.execute( + action="execute_command", + command="curl -O http://img.netbian.com/file/2025/0716/091412RIFD9.jpg", + ) print(result) # 执行see_image操作 - result = await sb_vision_tool.execute(action="see_image", file_path="091412RIFD9.jpg") + result = await sb_vision_tool.execute( + action="see_image", file_path="091412RIFD9.jpg" + ) print(result) # 清理资源(可选) # await sb_shell_tool.cleanup() + if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main())