Apply pre-commit formatting fixes

This commit is contained in:
GhostC
2025-07-27 22:26:50 +08:00
parent 66f4ae046e
commit a010fe7048
27 changed files with 340 additions and 244 deletions
+4 -1
View File
@@ -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
-2
View File
@@ -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."""
+3 -10
View File
@@ -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
-1
View File
@@ -55,4 +55,3 @@
## Learn More
- [Daytona Documentation](https://www.daytona.io/docs/)
+6 -15
View File
@@ -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}"
+1 -1
View File
@@ -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")
+3 -7
View File
@@ -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(
+9 -5
View File
@@ -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."""
+212 -105
View File
@@ -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:
+1
View File
@@ -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.
+9 -7
View File
@@ -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]":
+8 -7
View File
@@ -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
+9 -9
View File
@@ -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:
+1 -1
View File
@@ -1 +1 @@
# Utility functions and constants for agent tools
# Utility functions and constants for agent tools
+10 -14
View File
@@ -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
+5 -1
View File
@@ -1,4 +1,8 @@
import structlog, logging, os
import logging
import os
import structlog
ENV_MODE = os.getenv("ENV_MODE", "LOCAL")
-1
View File
@@ -192,4 +192,3 @@ Response:
## Learn More
- [A2A Protocol Documentation](https://google.github.io/A2A/#/documentation)
-1
View File
@@ -192,4 +192,3 @@ Response:
## Learn More
- [A2A Protocol Documentation](https://google.github.io/A2A/#/documentation)
+3 -3
View File
@@ -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)
+6 -8
View File
@@ -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__)
+11 -14
View File
@@ -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()
+6 -3
View File
@@ -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()) # 运行异步主函数
+2 -7
View File
@@ -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()
+1 -5
View File
@@ -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():
+12 -6
View File
@@ -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())
asyncio.run(main())
+6 -4
View File
@@ -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())
asyncio.run(main())
+12 -6
View File
@@ -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())
asyncio.run(main())