fix: resolve multiple type errors and runtime bugs

- shell.py: Fix FunctionTool import from correct module (core.agent.tool)
- deerflow/coze agent runners: Remove invalid return type annotations for step/step_until_done
- aiocqhttp_message_event.py: Fix raise string error (must raise Exception not str)
- lark_event.py: Fix file upload by wrapping bytes in BytesIO for SDK compatibility
- tg_adapter.py: Fix update.message None check in nested function
- TraceDisplayer.vue: Fix text color visibility using proper theme variables
This commit is contained in:
LIghtJUNction
2026-03-30 01:08:43 +08:00
parent de35ff5cda
commit 95cb2a8ac6
13 changed files with 72 additions and 82 deletions

View File

@@ -6,11 +6,12 @@ from typing import TYPE_CHECKING, Any
try:
import mcp
except (ModuleNotFoundError, ImportError):
mcp = None # type: ignore
mcp: Any = None
from astrbot._internal.tools.base import FunctionTool
if TYPE_CHECKING:
from mcp.types import Tool as MCPTool_T
from astrbot._internal.protocols.mcp.client import McpClient
@@ -19,8 +20,8 @@ class MCPTool(FunctionTool):
def __init__(
self,
mcp_tool: "mcp.types.Tool",
mcp_client: "McpClient",
mcp_tool: MCPTool_T,
mcp_client: McpClient,
mcp_server_name: str,
**kwargs: Any,
) -> None:

View File

@@ -27,6 +27,9 @@ class ToolSchema:
parameters: ParametersType = field(default_factory=dict)
"""The parameters of the tool, in JSON Schema format."""
active: bool = True
"""Whether the tool is active."""
@model_validator(mode="after")
def validate_parameters(self) -> "ToolSchema":
"""Validate the parameters JSON schema."""
@@ -54,12 +57,6 @@ class FunctionTool(ToolSchema):
causing the handler's __module__ to be functools
"""
active: bool = True
"""
Whether the tool is active. This field is a special field for AstrBot.
You can ignore it when integrating with other frameworks.
"""
is_background_task: bool = False
"""
Declare this tool as a background task. Background tasks return immediately
@@ -90,22 +87,22 @@ class ToolSet:
as "namespace/tool_name" when calling.
"""
def __init__(self, namespace: str, tools: list[FunctionTool] | None = None) -> None:
def __init__(self, namespace: str, tools: list[ToolSchema] | None = None) -> None:
self.namespace = namespace
self._tools: dict[str, FunctionTool] = {}
self._tools: dict[str, ToolSchema] = {}
if tools:
for tool in tools:
self.add(tool)
def add(self, tool: FunctionTool) -> None:
def add(self, tool: ToolSchema) -> None:
"""Add a tool to the set."""
self._tools[tool.name] = tool
def add_tool(self, tool: FunctionTool) -> None:
def add_tool(self, tool: ToolSchema) -> None:
"""Add a tool to the set (alias for add())."""
self.add(tool)
def remove(self, name: str) -> FunctionTool | None:
def remove(self, name: str) -> ToolSchema | None:
"""Remove and return a tool by name."""
return self._tools.pop(name, None)
@@ -113,19 +110,19 @@ class ToolSet:
"""Remove a tool by its name."""
self._tools.pop(name, None)
def get(self, name: str) -> FunctionTool | None:
def get(self, name: str) -> ToolSchema | None:
"""Get a tool by name."""
return self._tools.get(name)
def get_tool(self, name: str) -> FunctionTool | None:
def get_tool(self, name: str) -> ToolSchema | None:
"""Get a tool by name (alias for get)."""
return self.get(name)
def list_tools(self) -> list[FunctionTool]:
def list_tools(self) -> list[ToolSchema]:
"""List all tools in this set."""
return list(self._tools.values())
def __iter__(self) -> Iterator[FunctionTool]:
def __iter__(self) -> Iterator[ToolSchema]:
return iter(self._tools.values())
def __len__(self) -> int:
@@ -195,7 +192,7 @@ class ToolSet:
return ToolSet("default", param_tools)
@property
def tools(self) -> list[FunctionTool]:
def tools(self) -> list[ToolSchema]:
"""List all tools in this set."""
return list(self._tools.values())
@@ -205,18 +202,20 @@ class ToolSet:
"""Convert tools to OpenAI API function calling schema format."""
result: list[dict[str, Any]] = []
for tool in self._tools.values():
func_def: dict[str, Any] = {
"type": "function",
"function": {"name": tool.name},
}
func_dict: dict[str, Any] = {"name": tool.name}
if tool.description:
func_def["function"]["description"] = tool.description
func_dict["description"] = tool.description
if tool.parameters is not None:
if (
tool.parameters.get("properties")
) or not omit_empty_parameter_field:
func_def["function"]["parameters"] = tool.parameters
func_dict["parameters"] = tool.parameters
func_def: dict[str, Any] = {
"type": "function",
"function": func_dict,
}
result.append(func_def)
return result

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from typing import Any
# Re-export from base
from astrbot._internal.tools.base import FunctionTool, ToolSet
from astrbot._internal.tools.base import FunctionTool, ToolSchema, ToolSet
__all__ = [
"DEFAULT_MCP_CONFIG",
@@ -48,19 +48,19 @@ class FunctionToolManager:
"""Central registry for all function tools."""
def __init__(self) -> None:
self._func_list: list[FunctionTool] = []
self._func_list: list[ToolSchema] = []
@property
def func_list(self) -> list[FunctionTool]:
def func_list(self) -> list[ToolSchema]:
"""Get the list of function tools."""
return self._func_list
@func_list.setter
def func_list(self, value: list[FunctionTool]) -> None:
def func_list(self, value: list[ToolSchema]) -> None:
"""Set the list of function tools."""
self._func_list = value
def add(self, tool: FunctionTool) -> None:
def add(self, tool: ToolSchema) -> None:
"""Add a tool to the registry."""
self._func_list.append(tool)
@@ -72,9 +72,9 @@ class FunctionToolManager:
return True
return False
def get_func(self, name: str) -> FunctionTool | None:
def get_func(self, name: str) -> ToolSchema | None:
"""Get a tool by name. Returns the last active tool if multiple match."""
last_match: FunctionTool | None = None
last_match: ToolSchema | None = None
for f in reversed(self._func_list):
if f.name == name:
if getattr(f, "active", True):
@@ -85,7 +85,7 @@ class FunctionToolManager:
def get_full_tool_set(self) -> ToolSet:
"""Return a ToolSet with all active tools, deduplicated by name."""
seen: dict[str, FunctionTool] = {}
seen: dict[str, ToolSchema] = {}
for tool in reversed(self._func_list):
if tool.name not in seen and getattr(tool, "active", True):
seen[tool.name] = tool
@@ -98,7 +98,7 @@ class FunctionToolManager:
for tool in get_all_tools():
if self.get_func(tool.name) is None:
self.add(tool)
self.add(tool) # type: ignore[arg-type]
# MCP-related stub methods for base class compatibility
async def enable_mcp_server(
@@ -205,7 +205,7 @@ class FuncCall(FunctionToolManager):
"""Remove a function tool by name (deprecated, use remove() instead)."""
self.remove(name)
def get_func(self, name: str) -> FunctionTool | None:
def get_func(self, name: str) -> ToolSchema | None:
"""Get a function tool by name."""
return super().get_func(name)

View File

@@ -22,10 +22,10 @@ from functools import wraps
from typing import Any
# Import from _internal package (the canonical source)
from astrbot._internal.tools.base import FunctionTool, ToolSet
from astrbot._internal.tools.base import FunctionTool, ToolSchema, ToolSet
from astrbot._internal.tools.registry import FunctionToolManager
__all__ = ["FunctionTool", "ToolRegistry", "ToolSet", "get_registry", "tool"]
__all__ = ["FunctionTool", "ToolRegistry", "ToolSet", "get_registry", "tool", "ToolSchema"]
class ToolRegistry:
@@ -62,11 +62,11 @@ class ToolRegistry:
return True
return False
def list_tools(self) -> list[FunctionTool]:
def list_tools(self) -> list[ToolSchema]:
"""List all registered tools."""
return self._manager.func_list.copy()
def get_tool(self, name: str) -> FunctionTool | None:
def get_tool(self, name: str) -> ToolSchema | None:
"""Get a tool by name."""
return self._manager.get_func(name)

View File

@@ -552,7 +552,7 @@ class Main(star.Star):
tool_set = req.func_tool
if isinstance(tool_set, FunctionToolManager):
req.func_tool = tool_set.get_full_tool_set()
req.func_tool = tool_set.get_full_tool_set() # type: ignore[assignment]
tool_set = req.func_tool
if not tool_set:
@@ -569,9 +569,9 @@ class Main(star.Star):
web_search_t = func_tool_mgr.get_func("web_search")
fetch_url_t = func_tool_mgr.get_func("fetch_url")
if web_search_t and web_search_t.active:
tool_set.add_tool(web_search_t)
tool_set.add_tool(web_search_t) # type: ignore[arg-type]
if fetch_url_t and fetch_url_t.active:
tool_set.add_tool(fetch_url_t)
tool_set.add_tool(fetch_url_t) # type: ignore[arg-type]
tool_set.remove_tool("web_search_tavily")
tool_set.remove_tool("tavily_extract_web_page")
tool_set.remove_tool("AIsearch")
@@ -580,9 +580,9 @@ class Main(star.Star):
web_search_tavily = func_tool_mgr.get_func("web_search_tavily")
tavily_extract_web_page = func_tool_mgr.get_func("tavily_extract_web_page")
if web_search_tavily and web_search_tavily.active:
tool_set.add_tool(web_search_tavily)
tool_set.add_tool(web_search_tavily) # type: ignore[arg-type]
if tavily_extract_web_page and tavily_extract_web_page.active:
tool_set.add_tool(tavily_extract_web_page)
tool_set.add_tool(tavily_extract_web_page) # type: ignore[arg-type]
tool_set.remove_tool("web_search")
tool_set.remove_tool("fetch_url")
tool_set.remove_tool("AIsearch")
@@ -592,7 +592,7 @@ class Main(star.Star):
await self.ensure_baidu_ai_search_mcp(event.unified_msg_origin)
aisearch_tool = func_tool_mgr.get_func("AIsearch")
if aisearch_tool and aisearch_tool.active:
tool_set.add_tool(aisearch_tool)
tool_set.add_tool(aisearch_tool) # type: ignore[arg-type]
tool_set.remove_tool("web_search")
tool_set.remove_tool("fetch_url")
tool_set.remove_tool("web_search_tavily")
@@ -603,7 +603,7 @@ class Main(star.Star):
elif provider == "bocha":
web_search_bocha = func_tool_mgr.get_func("web_search_bocha")
if web_search_bocha and web_search_bocha.active:
tool_set.add_tool(web_search_bocha)
tool_set.add_tool(web_search_bocha) # type: ignore[arg-type]
tool_set.remove_tool("web_search")
tool_set.remove_tool("fetch_url")
tool_set.remove_tool("AIsearch")

View File

@@ -21,7 +21,7 @@ import sys
import warnings
from contextlib import AsyncExitStack
from datetime import timedelta
from typing import Any, Generic
from typing import Any, Generic, cast
from tenacity import (
before_sleep_log,
@@ -265,7 +265,7 @@ class MCPClient:
mcp.ClientSession(
*streams,
read_timeout_seconds=read_timeout,
logging_callback=logging_callback,
logging_callback=logging_callback, # type: ignore[arg-type]
),
)
else:
@@ -291,7 +291,7 @@ class MCPClient:
read_stream=read_s,
write_stream=write_s,
read_timeout_seconds=read_timeout,
logging_callback=logging_callback,
logging_callback=logging_callback, # type: ignore[arg-type]
),
)
@@ -317,11 +317,14 @@ class MCPClient:
stdio_transport = await self.exit_stack.enter_async_context(
mcp.stdio_client(
server_params,
errlog=LogPipe(
level=logging.INFO,
logger=logger,
identifier=f"MCPServer-{name}",
callback=callback,
errlog=cast(
Any,
LogPipe(
level=logging.INFO,
logger=logger,
identifier=f"MCPServer-{name}",
callback=callback,
),
),
),
)

View File

@@ -1,7 +1,6 @@
import base64
import json
import sys
from collections.abc import AsyncGenerator
from typing import Any
import astrbot.core.message.components as Comp
@@ -86,7 +85,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]):
self.file_id_cache: dict[str, dict[str, str]] = {}
@override
async def step(self) -> AsyncGenerator[AgentResponse, None]:
async def step(self):
"""
执行 Coze Agent 的一个步骤
"""
@@ -122,9 +121,7 @@ class CozeAgentRunner(BaseAgentRunner[TContext]):
await self.api_client.close()
@override
async def step_until_done(
self, max_step: int
) -> AsyncGenerator[AgentResponse, None]:
async def step_until_done(self, max_step: int):
while not self.done():
async for resp in self.step():
yield resp

View File

@@ -131,9 +131,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]):
)
@override
async def step_until_done(
self, max_step: int
) -> AsyncGenerator[AgentResponse, None]:
async def step_until_done(self, max_step: int):
while not self.done():
async for resp in self.step():
yield resp
@@ -260,7 +258,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]):
key="session_variables",
default={},
)
payload_vars.update(session_var)
payload_vars.update(session_var) # type: ignore[arg-type]
if (
self.dashscope_app_type in ["agent", "dialog-workflow"]

View File

@@ -4,7 +4,6 @@ import json
import sys
import typing as T
from collections import deque
from collections.abc import AsyncGenerator
from dataclasses import dataclass, field
from typing import Any
from uuid import uuid4
@@ -13,9 +12,9 @@ import astrbot.core.message.components as Comp
from astrbot import logger
from astrbot.core import sp
from astrbot.core.agent.hooks import BaseAgentRunHooks
from astrbot.core.agent.response import AgentResponseData
from astrbot.core.agent.response import AgentResponse, 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.base import AgentState, BaseAgentRunner
from astrbot.core.agent.tool_executor import BaseFunctionToolExecutor
from astrbot.core.message.message_event_result import MessageChain
from astrbot.core.provider.entities import (
@@ -319,9 +318,7 @@ class DeerFlowAgentRunner(BaseAgentRunner[TContext]):
yield await self._finish_with_error(err_msg)
@override
async def step_until_done(
self, max_step: int
) -> AsyncGenerator[AgentResponse, None]:
async def step_until_done(self, max_step: int):
if max_step <= 0:
raise ValueError("max_step must be greater than 0")

View File

@@ -114,9 +114,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]):
await self.api_client.close()
@override
async def step_until_done(
self, max_step: int
) -> AsyncGenerator[AgentResponse, None]:
async def step_until_done(self, max_step: int):
while not self.done():
async for resp in self.step():
yield resp
@@ -174,7 +172,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]):
key="session_variables",
default={},
)
payload_vars.update(session_var)
payload_vars.update(session_var) # type: ignore[arg-type]
payload_vars["system_prompt"] = system_prompt
# 处理不同的 API 类型
@@ -189,7 +187,7 @@ class DifyAgentRunner(BaseAgentRunner[TContext]):
},
query=prompt,
user=session_id,
conversation_id=conversation_id,
conversation_id=conversation_id or "",
files=files_payload,
request_timeout=self.timeout,
):

View File

@@ -6,7 +6,7 @@ import traceback
from collections.abc import AsyncGenerator, AsyncIterator
from contextlib import suppress
from dataclasses import dataclass, field
from typing import Any, Literal, TypeVar
from typing import Any, Literal, TypeVar, cast
from mcp.types import (
BlobResourceContents,
@@ -609,9 +609,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
self.req.append_tool_calls_result(tool_calls_result)
async def step_until_done(
self, max_step: int
) -> AsyncGenerator[AgentResponse, None]:
async def step_until_done(self, max_step: int):
"""Process steps until the agent is done."""
step_count = 0
max_step = min(max_step, 3)
@@ -772,7 +770,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
)
_final_resp: CallToolResult | None = None
async for resp in self._iter_tool_executor_results(executor):
async for resp in self._iter_tool_executor_results(executor): # type: ignore[arg-type]
if isinstance(resp, CallToolResult):
res = resp
_final_resp = resp

View File

@@ -25,9 +25,8 @@ import subprocess
from dataclasses import dataclass, field
from typing import Any
from astrbot.api import FunctionTool
from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.agent.tool import ToolExecResult
from astrbot.core.agent.tool import FunctionTool, ToolExecResult
from astrbot.core.astr_agent_context import AstrAgentContext
from .permissions import check_admin_permission

View File

@@ -49,7 +49,7 @@ class TestShipyardNeoBooterCapabilities:
caps = booter.capabilities
assert isinstance(caps, tuple)
with pytest.raises(AttributeError):
caps.append("mutated") # type: ignore[attr-defined]
caps.append("mutated")
# ═══════════════════════════════════════════════════════════════