feat: sandbox
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"booter": {
|
||||
"description": "Select booter to initialize the sandbox environment.",
|
||||
"type": "string",
|
||||
"options": [
|
||||
"shipyard-bay"
|
||||
],
|
||||
"default": "shipyard-bay"
|
||||
},
|
||||
"endpoint": {
|
||||
"description": "The endpoint URL of the Shipyard server.",
|
||||
"type": "string",
|
||||
"condition": {
|
||||
"booter": "shipyard-bay"
|
||||
}
|
||||
},
|
||||
"access_token": {
|
||||
"description": "The access token for authenticating with the Shipyard server.",
|
||||
"type": "string",
|
||||
"condition": {
|
||||
"booter": "shipyard-bay"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import os
|
||||
|
||||
import astrbot.api.star as star
|
||||
from astrbot.api import AstrBotConfig
|
||||
|
||||
from .tools.fs import CreateFileTool, FileUploadTool, ReadFileTool
|
||||
from .tools.python import PythonTool
|
||||
from .tools.shell import ExecuteShellTool
|
||||
|
||||
|
||||
class Main(star.Star):
|
||||
"""AstrBot Agent"""
|
||||
|
||||
def __init__(self, context: star.Context, config: AstrBotConfig) -> None:
|
||||
self.context = context
|
||||
self.config = config
|
||||
self.endpoint = config.get("endpoint", "http://localhost:8000")
|
||||
self.access_token = config.get("access_token", "")
|
||||
os.environ["ASTRBOT_SANDBOX_TYPE"] = config.get("booter", "shipyard-bay")
|
||||
os.environ["SHIPYARD_ENDPOINT"] = self.endpoint
|
||||
os.environ["SHIPYARD_ACCESS_TOKEN"] = self.access_token
|
||||
|
||||
context.add_llm_tools(
|
||||
CreateFileTool(),
|
||||
ExecuteShellTool(),
|
||||
PythonTool(),
|
||||
ReadFileTool(),
|
||||
FileUploadTool(),
|
||||
)
|
||||
@@ -1,4 +0,0 @@
|
||||
name: astrbot-agent
|
||||
desc: AstrBot Agent
|
||||
author: Soulter
|
||||
version: 0.0.1
|
||||
@@ -1,42 +0,0 @@
|
||||
import os
|
||||
import uuid
|
||||
from typing import Literal
|
||||
|
||||
from astrbot.api import logger
|
||||
|
||||
from .booters.base import SandboxBooter
|
||||
|
||||
|
||||
class SandboxClient:
|
||||
session_booter: dict[str, SandboxBooter] = {}
|
||||
|
||||
def __init__(
|
||||
self, booter_type: Literal["shipyard-bay", "boxlite"] | None = None
|
||||
) -> None:
|
||||
if booter_type is None:
|
||||
booter_type = os.getenv("ASTRBOT_SANDBOX_TYPE", "shipyard-bay") # type: ignore
|
||||
self.booter_type = booter_type
|
||||
logger.info(f"SandboxClient initialized with booter type: {self.booter_type}")
|
||||
|
||||
async def get_booter(self, session_id: str) -> SandboxBooter:
|
||||
if session_id not in self.session_booter:
|
||||
uuid_str = uuid.uuid5(uuid.NAMESPACE_DNS, session_id).hex
|
||||
if self.booter_type == "shipyard-bay":
|
||||
from .booters.shipyard import ShipyardBooter
|
||||
|
||||
self.client = ShipyardBooter()
|
||||
elif self.booter_type == "boxlite":
|
||||
from .booters.boxlite import ShipyardBooter
|
||||
|
||||
self.client = ShipyardBooter()
|
||||
else:
|
||||
raise ValueError(f"Unknown booter type: {self.booter_type}")
|
||||
|
||||
try:
|
||||
await self.client.boot(uuid_str)
|
||||
except Exception as e:
|
||||
logger.error(f"Error booting sandbox for session {session_id}: {e}")
|
||||
raise e
|
||||
|
||||
self.session_booter[session_id] = self.client
|
||||
return self.session_booter[session_id]
|
||||
@@ -113,6 +113,12 @@ DEFAULT_CONFIG = {
|
||||
"provider": "moonshotai",
|
||||
"moonshotai_api_key": "",
|
||||
},
|
||||
"sandbox": {
|
||||
"enable": False,
|
||||
"booter": "shipyard",
|
||||
"shipyard_endpoint": "",
|
||||
"shipyard_access_token": {},
|
||||
},
|
||||
},
|
||||
"provider_stt_settings": {
|
||||
"enable": False,
|
||||
@@ -2539,6 +2545,44 @@ CONFIG_METADATA_3 = {
|
||||
# "provider_settings.enable": True,
|
||||
# },
|
||||
# },
|
||||
"sandbox": {
|
||||
"description": "Agent 沙箱环境",
|
||||
"type": "object",
|
||||
"items": {
|
||||
"provider_settings.sandbox.enable": {
|
||||
"description": "启用沙箱环境",
|
||||
"type": "bool",
|
||||
"hint": "启用后,Agent 可以使用沙箱环境中的工具和资源,如 Python 代码执行、Shell 等。",
|
||||
},
|
||||
"provider_settings.sandbox.booter": {
|
||||
"description": "沙箱环境驱动器",
|
||||
"type": "string",
|
||||
"options": ["shipyard"],
|
||||
"condition": {
|
||||
"provider_settings.sandbox.enable": True,
|
||||
},
|
||||
},
|
||||
"provider_settings.sandbox.shipyard_endpoint": {
|
||||
"description": "Shipyard API Endpoint",
|
||||
"type": "string",
|
||||
"hint": "Shipyard 服务的 API 访问地址。",
|
||||
"condition": {
|
||||
"provider_settings.sandbox.enable": True,
|
||||
"provider_settings.sandbox.booter": "shipyard",
|
||||
},
|
||||
"_special": "check_shipyard_connection"
|
||||
},
|
||||
"provider_settings.sandbox.shipyard_api_key": {
|
||||
"description": "Shipyard API Key",
|
||||
"type": "string",
|
||||
"hint": "用于访问 Shipyard 服务的 API 密钥。",
|
||||
"condition": {
|
||||
"provider_settings.sandbox.enable": True,
|
||||
"provider_settings.sandbox.booter": "shipyard",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"truncate_and_compress": {
|
||||
"description": "上下文管理策略",
|
||||
"type": "object",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
from astrbot.core import logger
|
||||
@@ -35,8 +36,13 @@ from .....astr_agent_tool_exec import FunctionToolExecutor
|
||||
from ....context import PipelineContext, call_event_hook
|
||||
from ...stage import Stage
|
||||
from ...utils import (
|
||||
CREATE_FILE_TOOL,
|
||||
EXECUTE_SHELL_TOOL,
|
||||
FILE_UPLOAD_TOOL,
|
||||
KNOWLEDGE_BASE_QUERY_TOOL,
|
||||
LLM_SAFETY_MODE_SYSTEM_PROMPT,
|
||||
PYTHON_TOOL,
|
||||
READ_FILE_TOOL,
|
||||
retrieve_knowledge_base,
|
||||
)
|
||||
|
||||
@@ -93,6 +99,8 @@ class InternalAgentSubStage(Stage):
|
||||
"safety_mode_strategy", "system_prompt"
|
||||
)
|
||||
|
||||
self.sandbox_cfg = settings.get("sandbox", {})
|
||||
|
||||
self.conv_manager = ctx.plugin_manager.context.conversation_manager
|
||||
|
||||
def _select_provider(self, event: AstrMessageEvent):
|
||||
@@ -466,6 +474,24 @@ class InternalAgentSubStage(Stage):
|
||||
f"Unsupported llm_safety_mode strategy: {self.safety_mode_strategy}.",
|
||||
)
|
||||
|
||||
def _apply_sandbox_tools(self, req: ProviderRequest, session_id: str) -> None:
|
||||
"""Add sandbox tools to the provider request."""
|
||||
if req.func_tool is None:
|
||||
req.func_tool = ToolSet()
|
||||
if self.sandbox_cfg.get("booter") == "shipyard":
|
||||
ep = self.sandbox_cfg.get("shipyard_endpoint", "")
|
||||
at = self.sandbox_cfg.get("shipyard_access_token", "")
|
||||
if not ep or not at:
|
||||
logger.error("Shipyard sandbox configuration is incomplete.")
|
||||
return
|
||||
os.environ["SHIPYARD_ENDPOINT"] = ep
|
||||
os.environ["SHIPYARD_ACCESS_TOKEN"] = at
|
||||
req.func_tool.add_tool(CREATE_FILE_TOOL)
|
||||
req.func_tool.add_tool(READ_FILE_TOOL)
|
||||
req.func_tool.add_tool(EXECUTE_SHELL_TOOL)
|
||||
req.func_tool.add_tool(PYTHON_TOOL)
|
||||
req.func_tool.add_tool(FILE_UPLOAD_TOOL)
|
||||
|
||||
async def process(
|
||||
self, event: AstrMessageEvent, provider_wake_prefix: str
|
||||
) -> AsyncGenerator[None, None]:
|
||||
@@ -590,6 +616,10 @@ class InternalAgentSubStage(Stage):
|
||||
if self.llm_safety_mode:
|
||||
self._apply_llm_safety_mode(req)
|
||||
|
||||
# apply sandbox tools
|
||||
if self.sandbox_cfg.get("enable", False):
|
||||
self._apply_sandbox_tools(req, req.session_id)
|
||||
|
||||
stream_to_general = (
|
||||
self.unsupported_streaming_strategy == "turn_off"
|
||||
and not event.platform_meta.support_streaming_message
|
||||
|
||||
@@ -5,6 +5,13 @@ from astrbot.api import logger, sp
|
||||
from astrbot.core.agent.run_context import ContextWrapper
|
||||
from astrbot.core.agent.tool import FunctionTool, ToolExecResult
|
||||
from astrbot.core.astr_agent_context import AstrAgentContext
|
||||
from astrbot.core.sandbox.tools import (
|
||||
CreateFileTool,
|
||||
ExecuteShellTool,
|
||||
FileUploadTool,
|
||||
PythonTool,
|
||||
ReadFileTool,
|
||||
)
|
||||
from astrbot.core.star.context import Context
|
||||
|
||||
LLM_SAFETY_MODE_SYSTEM_PROMPT = """You are running in Safe Mode.
|
||||
@@ -135,3 +142,9 @@ async def retrieve_knowledge_base(
|
||||
|
||||
|
||||
KNOWLEDGE_BASE_QUERY_TOOL = KnowledgeBaseQueryTool()
|
||||
|
||||
CREATE_FILE_TOOL = CreateFileTool()
|
||||
READ_FILE_TOOL = ReadFileTool()
|
||||
EXECUTE_SHELL_TOOL = ExecuteShellTool()
|
||||
PYTHON_TOOL = PythonTool()
|
||||
FILE_UPLOAD_TOOL = FileUploadTool()
|
||||
|
||||
+4
-1
@@ -16,5 +16,8 @@ class SandboxBooter:
|
||||
async def shutdown(self) -> None: ...
|
||||
|
||||
async def upload_file(self, path: str, file_name: str) -> dict:
|
||||
"""Upload file to sandbox"""
|
||||
"""Upload file to sandbox.
|
||||
|
||||
Should return a dict with `success` (bool) and `file_path` (str) keys.
|
||||
"""
|
||||
...
|
||||
+1
-1
@@ -124,7 +124,7 @@ class MockShipyardSandboxClient:
|
||||
loop -= 1
|
||||
|
||||
|
||||
class ShipyardBooter(SandboxBooter):
|
||||
class BoxliteBooter(SandboxBooter):
|
||||
async def boot(self, session_id: str) -> None:
|
||||
logger.info(
|
||||
f"Booting(Boxlite) for session: {session_id}, this may take a while..."
|
||||
@@ -0,0 +1,38 @@
|
||||
import uuid
|
||||
from typing import Literal
|
||||
|
||||
from astrbot.api import logger
|
||||
|
||||
from .booters.base import SandboxBooter
|
||||
|
||||
session_booter: dict[str, SandboxBooter] = {}
|
||||
|
||||
|
||||
class SandboxClient:
|
||||
@classmethod
|
||||
async def get_booter(
|
||||
cls,
|
||||
session_id: str,
|
||||
booter_type: Literal["shipyard", "boxlite"] = "shipyard",
|
||||
) -> SandboxBooter:
|
||||
if session_id not in session_booter:
|
||||
uuid_str = uuid.uuid5(uuid.NAMESPACE_DNS, session_id).hex
|
||||
if booter_type == "shipyard":
|
||||
from .booters.shipyard import ShipyardBooter
|
||||
|
||||
client = ShipyardBooter()
|
||||
elif booter_type == "boxlite":
|
||||
from .booters.boxlite import BoxliteBooter
|
||||
|
||||
client = BoxliteBooter()
|
||||
else:
|
||||
raise ValueError(f"Unknown booter type: {booter_type}")
|
||||
|
||||
try:
|
||||
await client.boot(uuid_str)
|
||||
except Exception as e:
|
||||
logger.error(f"Error booting sandbox for session {session_id}: {e}")
|
||||
raise e
|
||||
|
||||
session_booter[session_id] = client
|
||||
return session_booter[session_id]
|
||||
@@ -0,0 +1,11 @@
|
||||
from .fs import CreateFileTool, FileUploadTool, ReadFileTool
|
||||
from .python import PythonTool
|
||||
from .shell import ExecuteShellTool
|
||||
|
||||
__all__ = [
|
||||
"CreateFileTool",
|
||||
"ReadFileTool",
|
||||
"FileUploadTool",
|
||||
"PythonTool",
|
||||
"ExecuteShellTool",
|
||||
]
|
||||
@@ -30,7 +30,7 @@ class CreateFileTool(FunctionTool):
|
||||
)
|
||||
|
||||
async def run(self, event: AstrMessageEvent, path: str, content: str):
|
||||
sb = await SandboxClient().get_booter(event.unified_msg_origin)
|
||||
sb = await SandboxClient.get_booter(event.unified_msg_origin)
|
||||
try:
|
||||
result = await sb.fs.create_file(path, content)
|
||||
return json.dumps(result)
|
||||
@@ -56,7 +56,7 @@ class ReadFileTool(FunctionTool):
|
||||
)
|
||||
|
||||
async def run(self, event: AstrMessageEvent, path: str):
|
||||
sb = await SandboxClient().get_booter(event.unified_msg_origin)
|
||||
sb = await SandboxClient.get_booter(event.unified_msg_origin)
|
||||
try:
|
||||
result = await sb.fs.read_file(path)
|
||||
return result
|
||||
@@ -90,7 +90,7 @@ class FileUploadTool(FunctionTool):
|
||||
event: AstrMessageEvent,
|
||||
local_path: str,
|
||||
):
|
||||
sb = await SandboxClient().get_booter(event.unified_msg_origin)
|
||||
sb = await SandboxClient.get_booter(event.unified_msg_origin)
|
||||
try:
|
||||
# Check if file exists
|
||||
if not os.path.exists(local_path):
|
||||
+1
-1
@@ -31,7 +31,7 @@ class PythonTool(FunctionTool):
|
||||
)
|
||||
|
||||
async def run(self, event: AstrMessageEvent, code: str, silent: bool = False):
|
||||
sb = await SandboxClient().get_booter(event.unified_msg_origin)
|
||||
sb = await SandboxClient.get_booter(event.unified_msg_origin)
|
||||
try:
|
||||
result = await sb.python.exec(code, silent=silent)
|
||||
output = result.get("output", {})
|
||||
+1
-1
@@ -42,7 +42,7 @@ class ExecuteShellTool(FunctionTool):
|
||||
background: bool = False,
|
||||
env: dict = {},
|
||||
):
|
||||
sb = await SandboxClient().get_booter(event.unified_msg_origin)
|
||||
sb = await SandboxClient.get_booter(event.unified_msg_origin)
|
||||
try:
|
||||
result = await sb.shell.exec(command, background=background, env=env)
|
||||
return json.dumps(result)
|
||||
@@ -133,6 +133,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sandbox": {
|
||||
"description": "Agent Sandbox Environment",
|
||||
"provider_settings": {
|
||||
"sandbox": {
|
||||
"enable": {
|
||||
"description": "Enable Sandbox Environment",
|
||||
"hint": "When enabled, Agent can use tools and resources in the sandbox environment, such as Python tool, Shell, etc."
|
||||
},
|
||||
"booter": {
|
||||
"description": "Sandbox Environment Driver"
|
||||
},
|
||||
"shipyard_endpoint": {
|
||||
"description": "Shipyard API Endpoint",
|
||||
"hint": "API access address for Shipyard service."
|
||||
},
|
||||
"shipyard_api_key": {
|
||||
"description": "Shipyard API Key",
|
||||
"hint": "API key for accessing Shipyard service."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncate_and_compress": {
|
||||
"description": "Context Management Strategy",
|
||||
"provider_settings": {
|
||||
|
||||
@@ -133,6 +133,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sandbox": {
|
||||
"description": "Agent 沙箱环境",
|
||||
"provider_settings": {
|
||||
"sandbox": {
|
||||
"enable": {
|
||||
"description": "启用沙箱环境",
|
||||
"hint": "启用后,Agent 可以使用沙箱环境中的工具和资源,如 Python 代码执行、Shell 等。"
|
||||
},
|
||||
"booter": {
|
||||
"description": "沙箱环境驱动器"
|
||||
},
|
||||
"shipyard_endpoint": {
|
||||
"description": "Shipyard API Endpoint",
|
||||
"hint": "Shipyard 服务的 API 访问地址。"
|
||||
},
|
||||
"shipyard_api_key": {
|
||||
"description": "Shipyard API Key",
|
||||
"hint": "用于访问 Shipyard 服务的 API 密钥。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"truncate_and_compress": {
|
||||
"description": "上下文管理策略",
|
||||
"provider_settings": {
|
||||
|
||||
Reference in New Issue
Block a user