mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
* feat: filesystem grep, read, edit file * feat: add file write tool and enhance file read functionality * feat: enhance tool prompt formatting and add escaped text decoding for file editing * feat: remove redundant safe path tests from security restrictions * feat: implement file read tool with support for text and image files, including validation for large files * feat: add file read utilities and integrate with filesystem tools * refactor: move computer tools to builtin tools registry * refactor: remove unused plugin_context parameter from _apply_sandbox_tools * feat: supports to display enabled builtin tools in configs * feat: add tooltip for disabled builtin tools and update localization strings * feat: add workspace extra prompt handling in message processing * feat: add ripgrep installation to Dockerfile * perf: shell executed in workspace dir in local env * feat: enhance file reading capabilities to support PDF and DOCX parsing, including workspace storage for long documents * feat: update converted text notice to suggest using grep for large files * feat: implement handling for large tool results with overflow file writing and read tool integration * fix: test * feat: enhance onboarding steps to include computer access configuration and related help information * feat: add support for additional temporary path in restricted environment checks * feat: update computer access hints and add detailed configuration instructions
329 lines
10 KiB
Python
329 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from importlib import import_module
|
|
from typing import Any, TypeVar
|
|
|
|
from astrbot.core.agent.tool import FunctionTool
|
|
|
|
TFunctionTool = TypeVar("TFunctionTool", bound=type[FunctionTool])
|
|
|
|
_BUILTIN_TOOL_MODULES = (
|
|
"astrbot.core.tools.computer_tools",
|
|
"astrbot.core.tools.cron_tools",
|
|
"astrbot.core.tools.knowledge_base_tools",
|
|
"astrbot.core.tools.message_tools",
|
|
"astrbot.core.tools.web_search_tools",
|
|
)
|
|
|
|
_builtin_tool_classes_by_name: dict[str, type[FunctionTool]] = {}
|
|
_builtin_tool_names_by_class: dict[type[FunctionTool], str] = {}
|
|
_builtin_tools_loaded = False
|
|
_MISSING = object()
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BuiltinToolConfigCondition:
|
|
key: str
|
|
operator: str
|
|
expected: Any = None
|
|
message: str | None = None
|
|
|
|
def evaluate(self, config: dict[str, Any]) -> dict[str, Any]:
|
|
actual = _get_config_value(config, self.key)
|
|
|
|
if self.operator == "equals":
|
|
matched = actual == self.expected
|
|
elif self.operator == "in":
|
|
expected_values = tuple(self.expected or ())
|
|
matched = actual in expected_values
|
|
elif self.operator == "truthy":
|
|
matched = bool(actual)
|
|
elif self.operator == "custom":
|
|
matched = bool(self.expected)
|
|
else:
|
|
raise ValueError(
|
|
f"Unsupported builtin tool config operator: {self.operator}"
|
|
)
|
|
|
|
return {
|
|
"key": self.key,
|
|
"operator": self.operator,
|
|
"expected": _json_safe(self.expected),
|
|
"actual": _json_safe(None if actual is _MISSING else actual),
|
|
"matched": matched,
|
|
"message": self.message,
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BuiltinToolConfigRule:
|
|
conditions: tuple[BuiltinToolConfigCondition, ...] = ()
|
|
evaluator: Callable[[dict[str, Any]], list[dict[str, Any]]] | None = None
|
|
|
|
def evaluate(self, config: dict[str, Any]) -> list[dict[str, Any]]:
|
|
if self.evaluator is not None:
|
|
return self.evaluator(config)
|
|
return [condition.evaluate(config) for condition in self.conditions]
|
|
|
|
|
|
def _get_config_value(config: dict[str, Any], key_path: str) -> Any:
|
|
current: Any = config
|
|
for segment in key_path.split("."):
|
|
if not isinstance(current, dict) or segment not in current:
|
|
return _MISSING
|
|
current = current[segment]
|
|
return current
|
|
|
|
|
|
def _json_safe(value: Any) -> Any:
|
|
if isinstance(value, tuple):
|
|
return [_json_safe(item) for item in value]
|
|
if isinstance(value, list):
|
|
return [_json_safe(item) for item in value]
|
|
if isinstance(value, dict):
|
|
return {key: _json_safe(val) for key, val in value.items()}
|
|
return value
|
|
|
|
|
|
def _equals(key: str, expected: Any) -> BuiltinToolConfigCondition:
|
|
return BuiltinToolConfigCondition(key=key, operator="equals", expected=expected)
|
|
|
|
|
|
def _in(key: str, expected: tuple[Any, ...]) -> BuiltinToolConfigCondition:
|
|
return BuiltinToolConfigCondition(key=key, operator="in", expected=expected)
|
|
|
|
|
|
def _custom_condition(key: str, *, matched: bool, message: str) -> dict[str, Any]:
|
|
return {
|
|
"key": key,
|
|
"operator": "custom",
|
|
"expected": None,
|
|
"actual": None,
|
|
"matched": matched,
|
|
"message": message,
|
|
}
|
|
|
|
|
|
def _build_rule_from_config_map(
|
|
config_map: dict[str, Any],
|
|
) -> BuiltinToolConfigRule:
|
|
conditions: list[BuiltinToolConfigCondition] = []
|
|
for key, expected in config_map.items():
|
|
if isinstance(expected, tuple):
|
|
conditions.append(_in(key, expected))
|
|
else:
|
|
conditions.append(_equals(key, expected))
|
|
return BuiltinToolConfigRule(conditions=tuple(conditions))
|
|
|
|
|
|
def _evaluate_send_message_tool(config: dict[str, Any]) -> list[dict[str, Any]]:
|
|
platform_configs = config.get("platform", [])
|
|
if not isinstance(platform_configs, list):
|
|
return [
|
|
_custom_condition(
|
|
"platform",
|
|
matched=False,
|
|
message="No enabled platform in this config supports proactive messaging.",
|
|
)
|
|
]
|
|
|
|
for platform_cfg in platform_configs:
|
|
if not isinstance(platform_cfg, dict):
|
|
continue
|
|
if platform_cfg.get("enable", False) is False:
|
|
continue
|
|
|
|
platform_type = str(platform_cfg.get("type", "")).strip()
|
|
platform_id = str(platform_cfg.get("id", "")).strip() or platform_type
|
|
if not platform_type:
|
|
continue
|
|
|
|
if platform_type in {"wecom", "weixin_official_account"}:
|
|
continue
|
|
|
|
if platform_type == "wecom_ai_bot":
|
|
webhook = str(platform_cfg.get("msg_push_webhook_url", "")).strip()
|
|
if not webhook:
|
|
continue
|
|
return [
|
|
_custom_condition(
|
|
"platform[].type",
|
|
matched=True,
|
|
message=(
|
|
f"Enabled platform `{platform_id}` uses `wecom_ai_bot`, which supports proactive messaging "
|
|
"when `platform[].msg_push_webhook_url` is configured."
|
|
),
|
|
),
|
|
BuiltinToolConfigCondition(
|
|
key="platform[].msg_push_webhook_url",
|
|
operator="truthy",
|
|
).evaluate({"platform[]": {"msg_push_webhook_url": webhook}}),
|
|
]
|
|
|
|
return [
|
|
_custom_condition(
|
|
"platform[].type",
|
|
matched=True,
|
|
message=(
|
|
f"Enabled platform `{platform_id}` (`{platform_type}`) supports proactive messaging."
|
|
),
|
|
)
|
|
]
|
|
|
|
return [
|
|
_custom_condition(
|
|
"platform",
|
|
matched=False,
|
|
message="No enabled platform in this config supports proactive messaging.",
|
|
)
|
|
]
|
|
|
|
|
|
_BUILTIN_TOOL_CONFIG_RULES: dict[str, BuiltinToolConfigRule] = {}
|
|
|
|
|
|
def _register_builtin_tool_config_rule(
|
|
tool_names: tuple[str, ...],
|
|
rule: BuiltinToolConfigRule,
|
|
) -> None:
|
|
for tool_name in tool_names:
|
|
_BUILTIN_TOOL_CONFIG_RULES[tool_name] = rule
|
|
|
|
|
|
_register_builtin_tool_config_rule(
|
|
("send_message_to_user",),
|
|
BuiltinToolConfigRule(evaluator=_evaluate_send_message_tool),
|
|
)
|
|
|
|
|
|
def _resolve_builtin_tool_name(tool_cls: type[FunctionTool]) -> str:
|
|
tool_name = getattr(tool_cls, "name", None)
|
|
if isinstance(tool_name, str) and tool_name:
|
|
return tool_name
|
|
|
|
dataclass_fields = getattr(tool_cls, "__dataclass_fields__", {})
|
|
name_field = dataclass_fields.get("name")
|
|
if name_field is not None and isinstance(name_field.default, str):
|
|
return name_field.default
|
|
|
|
raise ValueError(
|
|
f"Builtin tool class {tool_cls.__module__}.{tool_cls.__name__} does not define a valid name.",
|
|
)
|
|
|
|
|
|
def builtin_tool(
|
|
tool_cls: TFunctionTool | None = None,
|
|
*,
|
|
config: dict[str, Any] | None = None,
|
|
) -> TFunctionTool | Callable[[TFunctionTool], TFunctionTool]:
|
|
def _register(cls: TFunctionTool) -> TFunctionTool:
|
|
tool_name = _resolve_builtin_tool_name(cls)
|
|
existing = _builtin_tool_classes_by_name.get(tool_name)
|
|
if existing is not None and existing is not cls:
|
|
raise ValueError(
|
|
f"Builtin tool name conflict detected: {tool_name} is already registered by "
|
|
f"{existing.__module__}.{existing.__name__}.",
|
|
)
|
|
|
|
_builtin_tool_classes_by_name[tool_name] = cls
|
|
_builtin_tool_names_by_class[cls] = tool_name
|
|
if config is not None:
|
|
_BUILTIN_TOOL_CONFIG_RULES[tool_name] = _build_rule_from_config_map(config)
|
|
return cls
|
|
|
|
if tool_cls is None:
|
|
return _register
|
|
return _register(tool_cls)
|
|
|
|
|
|
def ensure_builtin_tools_loaded() -> None:
|
|
global _builtin_tools_loaded
|
|
if _builtin_tools_loaded:
|
|
return
|
|
|
|
for module_name in _BUILTIN_TOOL_MODULES:
|
|
import_module(module_name)
|
|
|
|
_builtin_tools_loaded = True
|
|
|
|
|
|
def get_builtin_tool_class(name: str) -> type[FunctionTool] | None:
|
|
ensure_builtin_tools_loaded()
|
|
return _builtin_tool_classes_by_name.get(name)
|
|
|
|
|
|
def get_builtin_tool_name(tool_cls: type[FunctionTool]) -> str | None:
|
|
ensure_builtin_tools_loaded()
|
|
return _builtin_tool_names_by_class.get(tool_cls)
|
|
|
|
|
|
def iter_builtin_tool_classes() -> tuple[type[FunctionTool], ...]:
|
|
ensure_builtin_tools_loaded()
|
|
return tuple(_builtin_tool_classes_by_name.values())
|
|
|
|
|
|
def get_builtin_tool_config_rule(name: str) -> BuiltinToolConfigRule | None:
|
|
ensure_builtin_tools_loaded()
|
|
return _BUILTIN_TOOL_CONFIG_RULES.get(name)
|
|
|
|
|
|
def get_builtin_tool_config_statuses(
|
|
tool_name: str,
|
|
config_entries: list[dict[str, Any]],
|
|
) -> list[dict[str, Any]]:
|
|
rule = get_builtin_tool_config_rule(tool_name)
|
|
if rule is None:
|
|
return []
|
|
|
|
statuses: list[dict[str, Any]] = []
|
|
for entry in config_entries:
|
|
config = entry.get("config")
|
|
if not isinstance(config, dict):
|
|
continue
|
|
|
|
conditions = rule.evaluate(config)
|
|
enabled = bool(conditions) and all(
|
|
bool(condition.get("matched")) for condition in conditions
|
|
)
|
|
statuses.append(
|
|
{
|
|
"conf_id": entry.get("conf_id"),
|
|
"conf_name": entry.get("conf_name"),
|
|
"enabled": enabled,
|
|
"matched_conditions": [
|
|
condition for condition in conditions if condition.get("matched")
|
|
],
|
|
"failed_conditions": [
|
|
condition
|
|
for condition in conditions
|
|
if not condition.get("matched")
|
|
],
|
|
}
|
|
)
|
|
return statuses
|
|
|
|
|
|
def get_builtin_tool_config_tags(
|
|
tool_name: str,
|
|
config_entries: list[dict[str, Any]],
|
|
) -> list[dict[str, Any]]:
|
|
return [
|
|
status
|
|
for status in get_builtin_tool_config_statuses(tool_name, config_entries)
|
|
if status["enabled"]
|
|
]
|
|
|
|
|
|
__all__ = [
|
|
"builtin_tool",
|
|
"ensure_builtin_tools_loaded",
|
|
"get_builtin_tool_config_rule",
|
|
"get_builtin_tool_config_statuses",
|
|
"get_builtin_tool_config_tags",
|
|
"get_builtin_tool_class",
|
|
"get_builtin_tool_name",
|
|
"iter_builtin_tool_classes",
|
|
]
|