mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-10-16 06:58:51 +08:00
feat: reduce default max_agent_step to 3 and refactor shell execution
- Lower default max_agent_step from 30 to 3 across all agent runners (coze, dashscope, deerflow, dify) for faster responses - Refactor ExecuteShellTool to use plumbum with session-based isolation, maintaining shell state per session (cwd, env vars, etc.) - Remove unused API-specific environment variables from cmd_run - Fix data access bug in shipyard_neo._maybe_model_dump - Add plumbum>=1.10.0 dependency
This commit is contained in:
@@ -65,3 +65,4 @@ GenieData/
|
||||
.worktrees/
|
||||
|
||||
.env
|
||||
dashboard/warker.js
|
||||
|
||||
@@ -306,13 +306,6 @@ def run(
|
||||
"ASTRBOT_SSL_CERT",
|
||||
"ASTRBOT_SSL_KEY",
|
||||
"ASTRBOT_SSL_CA_CERTS",
|
||||
# API specific envs
|
||||
"ASTRBOT_API_HOST",
|
||||
"ASTRBOT_API_PORT",
|
||||
"ASTRBOT_API_SSL_ENABLE",
|
||||
"ASTRBOT_API_SSL_CERT",
|
||||
"ASTRBOT_API_SSL_KEY",
|
||||
"ASTRBOT_API_SSL_CA_CERTS",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"no_proxy",
|
||||
@@ -341,6 +334,11 @@ def run(
|
||||
lock_file = astrbot_root / "astrbot.lock"
|
||||
lock = FileLock(lock_file, timeout=5)
|
||||
with lock.acquire():
|
||||
click.echo("AstrBot is running...")
|
||||
if backend_only:
|
||||
click.echo("Visit the dashboard at : https://dash.astrbot.men/")
|
||||
click.echo("Backend Requests : localhost or based on https")
|
||||
|
||||
asyncio.run(run_astrbot(astrbot_root))
|
||||
except KeyboardInterrupt:
|
||||
click.echo("AstrBot has been shut down.")
|
||||
|
||||
@@ -107,7 +107,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]):
|
||||
|
||||
@override
|
||||
async def step_until_done(
|
||||
self, max_step: int = 30
|
||||
self, max_step: int = 3
|
||||
) -> T.AsyncGenerator[AgentResponse, None]:
|
||||
while not self.done():
|
||||
async for resp in self.step():
|
||||
|
||||
@@ -117,7 +117,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]):
|
||||
|
||||
@override
|
||||
async def step_until_done(
|
||||
self, max_step: int = 30
|
||||
self, max_step: int = 3
|
||||
) -> T.AsyncGenerator[AgentResponse, None]:
|
||||
while not self.done():
|
||||
async for resp in self.step():
|
||||
|
||||
@@ -304,7 +304,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]):
|
||||
|
||||
@override
|
||||
async def step_until_done(
|
||||
self, max_step: int = 30
|
||||
self, max_step: int = 3
|
||||
) -> T.AsyncGenerator[AgentResponse, None]:
|
||||
if max_step <= 0:
|
||||
raise ValueError("max_step must be greater than 0")
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import base64
|
||||
import os
|
||||
import sys
|
||||
import typing as T
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
import astrbot.core.message.components as Comp
|
||||
from astrbot.core import logger, sp
|
||||
from astrbot.core.agent.hooks import BaseAgentRunHooks
|
||||
from astrbot.core.agent.response import AgentResponseData
|
||||
from astrbot.core.agent.run_context import ContextWrapper, TContext
|
||||
from astrbot.core.agent.runners.base import AgentResponse, AgentState, BaseAgentRunner
|
||||
from astrbot.core.agent.runners.dify.dify_api_client import DifyAPIClient
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
from astrbot.core.provider.entities import (
|
||||
LLMResponse,
|
||||
@@ -13,12 +19,6 @@ from astrbot.core.provider.entities import (
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||
from astrbot.core.utils.io import download_file
|
||||
|
||||
from ...hooks import BaseAgentRunHooks
|
||||
from ...response import AgentResponseData
|
||||
from ...run_context import ContextWrapper, TContext
|
||||
from ..base import AgentResponse, AgentState, BaseAgentRunner
|
||||
from .dify_api_client import DifyAPIClient
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
@@ -35,7 +35,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]):
|
||||
run_context: ContextWrapper[TContext],
|
||||
agent_hooks: BaseAgentRunHooks[TContext],
|
||||
provider_config: dict,
|
||||
**kwargs: T.Any,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.req = request
|
||||
self.streaming = kwargs.get("streaming", False)
|
||||
@@ -100,8 +100,8 @@ class DifyAgentRunner(BaseAgentRunner[TContext]):
|
||||
|
||||
@override
|
||||
async def step_until_done(
|
||||
self, max_step: int = 30
|
||||
) -> T.AsyncGenerator[AgentResponse, None]:
|
||||
self, max_step: int = 3
|
||||
) -> AsyncGenerator[AgentResponse, None]:
|
||||
while not self.done():
|
||||
async for resp in self.step():
|
||||
yield resp
|
||||
|
||||
@@ -106,7 +106,7 @@ def _extract_final_streaming_chain(msg_chain: MessageChain) -> MessageChain | No
|
||||
|
||||
async def run_agent(
|
||||
agent_runner: AgentRunner,
|
||||
max_step: int = 30,
|
||||
max_step: int = 3,
|
||||
show_tool_use: bool = True,
|
||||
show_tool_call_result: bool = False,
|
||||
stream_to_general: bool = False,
|
||||
@@ -301,7 +301,7 @@ async def _watch_agent_stop_signal(agent_runner: AgentRunner, astr_event) -> Non
|
||||
async def run_live_agent(
|
||||
agent_runner: AgentRunner,
|
||||
tts_provider: TTSProvider | None = None,
|
||||
max_step: int = 30,
|
||||
max_step: int = 3,
|
||||
show_tool_use: bool = True,
|
||||
show_tool_call_result: bool = False,
|
||||
show_reasoning: bool = False,
|
||||
|
||||
@@ -357,7 +357,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
|
||||
continue
|
||||
|
||||
prov_settings: dict = ctx.get_config(umo=umo).get("provider_settings", {})
|
||||
agent_max_step = int(prov_settings.get("max_agent_step", 30))
|
||||
agent_max_step = int(prov_settings.get("max_agent_step", 3))
|
||||
stream = prov_settings.get("streaming_response", False)
|
||||
llm_resp = await ctx.tool_loop_agent(
|
||||
event=event,
|
||||
@@ -583,7 +583,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
|
||||
return
|
||||
|
||||
runner = result.agent_runner
|
||||
async for _ in runner.step_until_done(30):
|
||||
async for _ in runner.step_until_done(3):
|
||||
# agent will send message to user via using tools
|
||||
pass
|
||||
llm_resp = runner.get_final_llm_resp()
|
||||
|
||||
@@ -12,13 +12,13 @@ from astrbot.api import logger
|
||||
if TYPE_CHECKING:
|
||||
from astrbot.core.agent.tool import FunctionTool
|
||||
|
||||
from ..olayer import (
|
||||
from astrbot.core.computer.booters.base import ComputerBooter
|
||||
from astrbot.core.computer.olayer import (
|
||||
BrowserComponent,
|
||||
FileSystemComponent,
|
||||
PythonComponent,
|
||||
ShellComponent,
|
||||
)
|
||||
from .base import ComputerBooter
|
||||
|
||||
|
||||
def _maybe_model_dump(value: Any) -> dict[str, Any]:
|
||||
@@ -49,7 +49,7 @@ class NeoPythonComponent(PythonComponent):
|
||||
output_text = payload.get("output", "") or ""
|
||||
error_text = payload.get("error", "") or ""
|
||||
data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
|
||||
rich_output = data.get("output") if isinstance(data.get("output"), dict) else {}
|
||||
rich_output = (data.get("output") or {}) if isinstance(data, dict) else {}
|
||||
if not isinstance(rich_output.get("images"), list):
|
||||
rich_output["images"] = []
|
||||
if "text" not in rich_output:
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from plumbum import local
|
||||
from plumbum.commands.processes import ProcessExecutionError
|
||||
|
||||
from astrbot.api import FunctionTool
|
||||
from astrbot.core.agent.run_context import ContextWrapper
|
||||
from astrbot.core.agent.tool import ToolExecResult
|
||||
from astrbot.core.astr_agent_context import AstrAgentContext
|
||||
|
||||
from ..computer_client import get_booter, get_local_booter
|
||||
from .permissions import check_admin_permission
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecuteShellTool(FunctionTool):
|
||||
"""Stateful shell execution tool based on plumbum.
|
||||
|
||||
Each session maintains its own shell instance, ensuring state isolation
|
||||
across different sessions (such as working directory, environment variables, etc.).
|
||||
"""
|
||||
|
||||
name: str = "astrbot_execute_shell"
|
||||
description: str = "Execute a command in the shell."
|
||||
parameters: dict = field(
|
||||
@@ -39,6 +50,24 @@ class ExecuteShellTool(FunctionTool):
|
||||
)
|
||||
|
||||
is_local: bool = False
|
||||
_session_shells: dict[str, Any] = field(
|
||||
default_factory=dict, init=False, repr=False
|
||||
)
|
||||
|
||||
def _get_session_shell(self, session_id: str) -> Any:
|
||||
"""Get or create a shell instance for a specific session.
|
||||
|
||||
Args:
|
||||
session_id: The unique session identifier.
|
||||
|
||||
Returns:
|
||||
plumbum shell instance for this session.
|
||||
"""
|
||||
import os
|
||||
|
||||
if session_id not in self._session_shells:
|
||||
self._session_shells[session_id] = local.cwd(os.getcwd())
|
||||
return self._session_shells[session_id]
|
||||
|
||||
async def call(
|
||||
self,
|
||||
@@ -50,26 +79,82 @@ class ExecuteShellTool(FunctionTool):
|
||||
if permission_error := check_admin_permission(context, "Shell execution"):
|
||||
return permission_error
|
||||
|
||||
if self.is_local:
|
||||
sb = get_local_booter()
|
||||
else:
|
||||
sb = await get_booter(
|
||||
context.context.context,
|
||||
context.context.event.unified_msg_origin,
|
||||
)
|
||||
try:
|
||||
config = context.context.context.get_config(
|
||||
umo=context.context.event.unified_msg_origin
|
||||
)
|
||||
# Get session ID for per-session shell isolation
|
||||
session_id = context.context.event.unified_msg_origin
|
||||
shell = self._get_session_shell(session_id)
|
||||
|
||||
config = context.context.context.get_config(umo=session_id)
|
||||
try:
|
||||
timeout = int(
|
||||
config.get("provider_settings", {}).get("tool_call_timeout", 30)
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
timeout = 30
|
||||
result = await sb.shell.exec(
|
||||
command, background=background, env=env, timeout=timeout
|
||||
)
|
||||
|
||||
# Merge environment variables
|
||||
if env:
|
||||
full_env = shell.env.copy()
|
||||
full_env.update(env)
|
||||
else:
|
||||
full_env = None
|
||||
|
||||
if background:
|
||||
# Background execution: use & to run in background, no waiting
|
||||
cmd_line = f"{command} &"
|
||||
if full_env:
|
||||
_, stdout, stderr = shell.run(
|
||||
["/bin/sh", "-c", cmd_line],
|
||||
env=full_env,
|
||||
timeout=timeout,
|
||||
)
|
||||
else:
|
||||
_, stdout, stderr = shell.run(
|
||||
["/bin/sh", "-c", cmd_line],
|
||||
timeout=timeout,
|
||||
)
|
||||
result = {
|
||||
"success": True,
|
||||
"background": True,
|
||||
"stdout": stdout,
|
||||
"stderr": stderr,
|
||||
}
|
||||
else:
|
||||
# Foreground execution: wait for command completion
|
||||
try:
|
||||
if full_env:
|
||||
exit_code, stdout, stderr = shell.run(
|
||||
["/bin/sh", "-c", command],
|
||||
env=full_env,
|
||||
timeout=timeout,
|
||||
)
|
||||
else:
|
||||
exit_code, stdout, stderr = shell.run(
|
||||
["/bin/sh", "-c", command],
|
||||
timeout=timeout,
|
||||
)
|
||||
result = {
|
||||
"success": exit_code == 0,
|
||||
"exit_code": exit_code,
|
||||
"stdout": stdout,
|
||||
"stderr": stderr,
|
||||
}
|
||||
except ProcessExecutionError as e:
|
||||
result = {
|
||||
"success": False,
|
||||
"exit_code": e.exit_code,
|
||||
"stdout": e.stdout,
|
||||
"stderr": e.stderr,
|
||||
}
|
||||
except Exception as e:
|
||||
result = {
|
||||
"success": False,
|
||||
"exit_code": -1,
|
||||
"stdout": "",
|
||||
"stderr": str(e),
|
||||
}
|
||||
|
||||
return json.dumps(result)
|
||||
|
||||
except Exception as e:
|
||||
return f"Error executing command: {e!s}"
|
||||
|
||||
@@ -3,3 +3,4 @@ node_modules/
|
||||
dist/
|
||||
bun.lock
|
||||
pnpm-lock.yaml
|
||||
worker.js
|
||||
|
||||
@@ -73,7 +73,7 @@ AstrBot 默认配置如下:
|
||||
"streaming_response": False,
|
||||
"show_tool_use_status": False,
|
||||
"streaming_segmented": False,
|
||||
"max_agent_step": 30,
|
||||
"max_agent_step": 3,
|
||||
"tool_call_timeout": 120,
|
||||
},
|
||||
"provider_stt_settings": {
|
||||
|
||||
@@ -67,6 +67,7 @@ dependencies = [
|
||||
"quart-cors>=0.8.0",
|
||||
"anyio>=4.12.1",
|
||||
"argon2-cffi>=23.1.0",
|
||||
"plumbum>=1.10.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
Reference in New Issue
Block a user