Apply pre-commit formatting fixes
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -55,4 +55,3 @@
|
||||
## Learn More
|
||||
|
||||
- [Daytona Documentation](https://www.daytona.io/docs/)
|
||||
|
||||
|
||||
+6
-15
@@ -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}"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 @@
|
||||
# Utility functions and constants for agent tools
|
||||
# Utility functions and constants for agent tools
|
||||
|
||||
+10
-14
@@ -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
@@ -1,4 +1,8 @@
|
||||
import structlog, logging, os
|
||||
import logging
|
||||
import os
|
||||
|
||||
import structlog
|
||||
|
||||
|
||||
ENV_MODE = os.getenv("ENV_MODE", "LOCAL")
|
||||
|
||||
|
||||
@@ -192,4 +192,3 @@ Response:
|
||||
## Learn More
|
||||
|
||||
- [A2A Protocol Documentation](https://google.github.io/A2A/#/documentation)
|
||||
|
||||
|
||||
@@ -192,4 +192,3 @@ Response:
|
||||
## Learn More
|
||||
|
||||
- [A2A Protocol Documentation](https://google.github.io/A2A/#/documentation)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
|
||||
@@ -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()) # 运行异步主函数
|
||||
|
||||
@@ -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,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():
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user