mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-18 10:00:40 +08:00
feat: supports to display enabled builtin tools in configs
This commit is contained in:
@@ -54,6 +54,13 @@ from astrbot.core.utils.astrbot_path import (
|
||||
from ..registry import builtin_tool
|
||||
from .permissions import check_admin_permission
|
||||
|
||||
_COMPUTER_RUNTIME_TOOL_CONFIG = {
|
||||
"provider_settings.computer_use_runtime": ("local", "sandbox"),
|
||||
}
|
||||
_SANDBOX_RUNTIME_TOOL_CONFIG = {
|
||||
"provider_settings.computer_use_runtime": "sandbox",
|
||||
}
|
||||
|
||||
|
||||
def _normalize_umo_for_workspace(umo: str) -> str:
|
||||
normalized = re.sub(r"[^A-Za-z0-9._-]+", "_", umo.strip())
|
||||
@@ -163,7 +170,7 @@ def _decode_escaped_text(value: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_COMPUTER_RUNTIME_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class FileReadTool(FunctionTool):
|
||||
name: str = "astrbot_file_read_tool"
|
||||
@@ -243,7 +250,7 @@ class FileReadTool(FunctionTool):
|
||||
return f"Error reading file: {exc}"
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_COMPUTER_RUNTIME_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class FileWriteTool(FunctionTool):
|
||||
name: str = "astrbot_file_write_tool"
|
||||
@@ -310,7 +317,7 @@ class FileWriteTool(FunctionTool):
|
||||
return f"Error writing file: {exc}"
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_COMPUTER_RUNTIME_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class FileEditTool(FunctionTool):
|
||||
name: str = "astrbot_file_edit_tool"
|
||||
@@ -396,7 +403,7 @@ class FileEditTool(FunctionTool):
|
||||
return f"Error editing file: {exc}"
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_COMPUTER_RUNTIME_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class GrepTool(FunctionTool):
|
||||
name: str = "astrbot_grep_tool"
|
||||
@@ -602,7 +609,7 @@ class GrepTool(FunctionTool):
|
||||
return f"Error searching files: {exc}"
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_SANDBOX_RUNTIME_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class FileUploadTool(FunctionTool):
|
||||
name: str = "astrbot_upload_file"
|
||||
@@ -668,7 +675,7 @@ class FileUploadTool(FunctionTool):
|
||||
return f"Error uploading file: {str(e)}"
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_SANDBOX_RUNTIME_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class FileDownloadTool(FunctionTool):
|
||||
name: str = "astrbot_download_file"
|
||||
|
||||
@@ -14,6 +14,12 @@ from ..registry import builtin_tool
|
||||
from .permissions import check_admin_permission
|
||||
|
||||
_OS_NAME = platform.system()
|
||||
_SANDBOX_PYTHON_TOOL_CONFIG = {
|
||||
"provider_settings.computer_use_runtime": "sandbox",
|
||||
}
|
||||
_LOCAL_PYTHON_TOOL_CONFIG = {
|
||||
"provider_settings.computer_use_runtime": "local",
|
||||
}
|
||||
|
||||
param_schema = {
|
||||
"type": "object",
|
||||
@@ -63,7 +69,7 @@ async def handle_result(result: dict, event: AstrMessageEvent) -> ToolExecResult
|
||||
return resp
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_SANDBOX_PYTHON_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class PythonTool(FunctionTool):
|
||||
name: str = "astrbot_execute_ipython"
|
||||
@@ -86,7 +92,7 @@ class PythonTool(FunctionTool):
|
||||
return f"Error executing code: {str(e)}"
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_LOCAL_PYTHON_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class LocalPythonTool(FunctionTool):
|
||||
name: str = "astrbot_execute_python"
|
||||
|
||||
@@ -10,8 +10,12 @@ from astrbot.core.computer.computer_client import get_booter
|
||||
from ..registry import builtin_tool
|
||||
from .permissions import check_admin_permission
|
||||
|
||||
_COMPUTER_RUNTIME_TOOL_CONFIG = {
|
||||
"provider_settings.computer_use_runtime": ("local", "sandbox"),
|
||||
}
|
||||
|
||||
@builtin_tool
|
||||
|
||||
@builtin_tool(config=_COMPUTER_RUNTIME_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class ExecuteShellTool(FunctionTool):
|
||||
name: str = "astrbot_execute_shell"
|
||||
|
||||
@@ -10,6 +10,11 @@ from astrbot.core.computer.computer_client import get_booter
|
||||
from astrbot.core.tools.computer_tools.permissions import check_admin_permission
|
||||
from astrbot.core.tools.registry import builtin_tool
|
||||
|
||||
_SHIPYARD_NEO_TOOL_CONFIG = {
|
||||
"provider_settings.computer_use_runtime": "sandbox",
|
||||
"provider_settings.sandbox.booter": "shipyard_neo",
|
||||
}
|
||||
|
||||
|
||||
def _to_json(data: Any) -> str:
|
||||
return json.dumps(data, ensure_ascii=False, default=str)
|
||||
@@ -29,7 +34,7 @@ async def _get_browser_component(context: ContextWrapper[AstrAgentContext]) -> A
|
||||
return browser
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class BrowserExecTool(FunctionTool):
|
||||
name: str = "astrbot_execute_browser"
|
||||
@@ -87,7 +92,7 @@ class BrowserExecTool(FunctionTool):
|
||||
return f"Error executing browser command: {str(e)}"
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class BrowserBatchExecTool(FunctionTool):
|
||||
name: str = "astrbot_execute_browser_batch"
|
||||
@@ -152,7 +157,7 @@ class BrowserBatchExecTool(FunctionTool):
|
||||
return f"Error executing browser batch command: {str(e)}"
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class RunBrowserSkillTool(FunctionTool):
|
||||
name: str = "astrbot_run_browser_skill"
|
||||
|
||||
@@ -12,6 +12,11 @@ from astrbot.core.skills.neo_skill_sync import NeoSkillSyncManager
|
||||
from astrbot.core.tools.computer_tools.permissions import check_admin_permission
|
||||
from astrbot.core.tools.registry import builtin_tool
|
||||
|
||||
_SHIPYARD_NEO_TOOL_CONFIG = {
|
||||
"provider_settings.computer_use_runtime": "sandbox",
|
||||
"provider_settings.sandbox.booter": "shipyard_neo",
|
||||
}
|
||||
|
||||
|
||||
def _to_jsonable(model_like: Any) -> Any:
|
||||
if isinstance(model_like, dict):
|
||||
@@ -64,7 +69,7 @@ class NeoSkillToolBase(FunctionTool):
|
||||
return f"{self.error_prefix} {error_action}: {str(e)}"
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class GetExecutionHistoryTool(NeoSkillToolBase):
|
||||
name: str = "astrbot_get_execution_history"
|
||||
@@ -111,7 +116,7 @@ class GetExecutionHistoryTool(NeoSkillToolBase):
|
||||
)
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class AnnotateExecutionTool(NeoSkillToolBase):
|
||||
name: str = "astrbot_annotate_execution"
|
||||
@@ -149,7 +154,7 @@ class AnnotateExecutionTool(NeoSkillToolBase):
|
||||
)
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class CreateSkillPayloadTool(NeoSkillToolBase):
|
||||
name: str = "astrbot_create_skill_payload"
|
||||
@@ -197,7 +202,7 @@ class CreateSkillPayloadTool(NeoSkillToolBase):
|
||||
)
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class GetSkillPayloadTool(NeoSkillToolBase):
|
||||
name: str = "astrbot_get_skill_payload"
|
||||
@@ -224,7 +229,7 @@ class GetSkillPayloadTool(NeoSkillToolBase):
|
||||
)
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class CreateSkillCandidateTool(NeoSkillToolBase):
|
||||
name: str = "astrbot_create_skill_candidate"
|
||||
@@ -278,7 +283,7 @@ class CreateSkillCandidateTool(NeoSkillToolBase):
|
||||
)
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class ListSkillCandidatesTool(NeoSkillToolBase):
|
||||
name: str = "astrbot_list_skill_candidates"
|
||||
@@ -316,7 +321,7 @@ class ListSkillCandidatesTool(NeoSkillToolBase):
|
||||
)
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class EvaluateSkillCandidateTool(NeoSkillToolBase):
|
||||
name: str = "astrbot_evaluate_skill_candidate"
|
||||
@@ -357,7 +362,7 @@ class EvaluateSkillCandidateTool(NeoSkillToolBase):
|
||||
)
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class PromoteSkillCandidateTool(NeoSkillToolBase):
|
||||
name: str = "astrbot_promote_skill_candidate"
|
||||
@@ -428,7 +433,7 @@ class PromoteSkillCandidateTool(NeoSkillToolBase):
|
||||
return f"Error promoting skill candidate: {str(e)}"
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class ListSkillReleasesTool(NeoSkillToolBase):
|
||||
name: str = "astrbot_list_skill_releases"
|
||||
@@ -469,7 +474,7 @@ class ListSkillReleasesTool(NeoSkillToolBase):
|
||||
)
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class RollbackSkillReleaseTool(NeoSkillToolBase):
|
||||
name: str = "astrbot_rollback_skill_release"
|
||||
@@ -496,7 +501,7 @@ class RollbackSkillReleaseTool(NeoSkillToolBase):
|
||||
)
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_SHIPYARD_NEO_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class SyncSkillReleaseTool(NeoSkillToolBase):
|
||||
name: str = "astrbot_sync_skill_release"
|
||||
|
||||
@@ -9,6 +9,10 @@ from astrbot.core.agent.tool import FunctionTool, ToolExecResult
|
||||
from astrbot.core.astr_agent_context import AstrAgentContext
|
||||
from astrbot.core.tools.registry import builtin_tool
|
||||
|
||||
_CRON_TOOL_CONFIG = {
|
||||
"provider_settings.proactive_capability.add_cron_tools": True,
|
||||
}
|
||||
|
||||
|
||||
def _extract_job_session(job: Any) -> str | None:
|
||||
payload = getattr(job, "payload", None)
|
||||
@@ -18,7 +22,7 @@ def _extract_job_session(job: Any) -> str | None:
|
||||
return str(session) if session is not None else None
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_CRON_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class CreateActiveCronTool(FunctionTool[AstrAgentContext]):
|
||||
name: str = "create_future_task"
|
||||
@@ -107,7 +111,7 @@ class CreateActiveCronTool(FunctionTool[AstrAgentContext]):
|
||||
return f"Scheduled future task {job.job_id} ({job.name}) {suffix}."
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_CRON_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class DeleteCronJobTool(FunctionTool[AstrAgentContext]):
|
||||
name: str = "delete_future_task"
|
||||
@@ -144,7 +148,7 @@ class DeleteCronJobTool(FunctionTool[AstrAgentContext]):
|
||||
return f"Deleted cron job {job_id}."
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_CRON_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class ListCronJobsTool(FunctionTool[AstrAgentContext]):
|
||||
name: str = "list_future_tasks"
|
||||
|
||||
@@ -9,6 +9,10 @@ from astrbot.core.knowledge_base.kb_helper import KBHelper
|
||||
from astrbot.core.star.context import Context
|
||||
from astrbot.core.tools.registry import builtin_tool
|
||||
|
||||
_KNOWLEDGE_BASE_TOOL_CONFIG = {
|
||||
"kb_agentic_mode": True,
|
||||
}
|
||||
|
||||
|
||||
def check_all_kb(kb_list: list[KBHelper | None]) -> bool:
|
||||
"""检查是否所有的知识库都为空"""
|
||||
@@ -83,7 +87,7 @@ async def retrieve_knowledge_base(
|
||||
return None
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_KNOWLEDGE_BASE_TOOL_CONFIG)
|
||||
@dataclass
|
||||
class KnowledgeBaseQueryTool(FunctionTool[AstrAgentContext]):
|
||||
name: str = "astr_kb_search"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from importlib import import_module
|
||||
from typing import TypeVar
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from astrbot.core.agent.tool import FunctionTool
|
||||
|
||||
@@ -18,6 +20,182 @@ _BUILTIN_TOOL_MODULES = (
|
||||
_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:
|
||||
@@ -35,18 +213,29 @@ def _resolve_builtin_tool_name(tool_cls: type[FunctionTool]) -> str:
|
||||
)
|
||||
|
||||
|
||||
def builtin_tool(tool_cls: TFunctionTool) -> TFunctionTool:
|
||||
tool_name = _resolve_builtin_tool_name(tool_cls)
|
||||
existing = _builtin_tool_classes_by_name.get(tool_name)
|
||||
if existing is not None and existing is not tool_cls:
|
||||
raise ValueError(
|
||||
f"Builtin tool name conflict detected: {tool_name} is already registered by "
|
||||
f"{existing.__module__}.{existing.__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] = tool_cls
|
||||
_builtin_tool_names_by_class[tool_cls] = tool_name
|
||||
return tool_cls
|
||||
_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:
|
||||
@@ -75,9 +264,64 @@ def iter_builtin_tool_classes() -> tuple[type[FunctionTool], ...]:
|
||||
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",
|
||||
|
||||
@@ -20,6 +20,22 @@ WEB_SEARCH_TOOL_NAMES = [
|
||||
"web_search_bocha",
|
||||
"web_search_brave",
|
||||
]
|
||||
_TAVILY_WEB_SEARCH_TOOL_CONFIG = {
|
||||
"provider_settings.web_search": True,
|
||||
"provider_settings.websearch_provider": "tavily",
|
||||
}
|
||||
_BOCHA_WEB_SEARCH_TOOL_CONFIG = {
|
||||
"provider_settings.web_search": True,
|
||||
"provider_settings.websearch_provider": "bocha",
|
||||
}
|
||||
_BRAVE_WEB_SEARCH_TOOL_CONFIG = {
|
||||
"provider_settings.web_search": True,
|
||||
"provider_settings.websearch_provider": "brave",
|
||||
}
|
||||
_BAIDU_WEB_SEARCH_TOOL_CONFIG = {
|
||||
"provider_settings.web_search": True,
|
||||
"provider_settings.websearch_provider": "baidu_ai_search",
|
||||
}
|
||||
|
||||
|
||||
@std_dataclass
|
||||
@@ -276,7 +292,7 @@ async def _baidu_search(
|
||||
]
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_TAVILY_WEB_SEARCH_TOOL_CONFIG)
|
||||
@pydantic_dataclass
|
||||
class TavilyWebSearchTool(FunctionTool[AstrAgentContext]):
|
||||
name: str = "web_search_tavily"
|
||||
@@ -359,7 +375,7 @@ class TavilyWebSearchTool(FunctionTool[AstrAgentContext]):
|
||||
return _search_result_payload(results)
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_TAVILY_WEB_SEARCH_TOOL_CONFIG)
|
||||
@pydantic_dataclass
|
||||
class TavilyExtractWebPageTool(FunctionTool[AstrAgentContext]):
|
||||
name: str = "tavily_extract_web_page"
|
||||
@@ -406,7 +422,7 @@ class TavilyExtractWebPageTool(FunctionTool[AstrAgentContext]):
|
||||
return ret or "Error: Tavily web searcher does not return any results."
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_BOCHA_WEB_SEARCH_TOOL_CONFIG)
|
||||
@pydantic_dataclass
|
||||
class BochaWebSearchTool(FunctionTool[AstrAgentContext]):
|
||||
name: str = "web_search_bocha"
|
||||
@@ -470,7 +486,7 @@ class BochaWebSearchTool(FunctionTool[AstrAgentContext]):
|
||||
return _search_result_payload(results)
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_BRAVE_WEB_SEARCH_TOOL_CONFIG)
|
||||
@pydantic_dataclass
|
||||
class BraveWebSearchTool(FunctionTool[AstrAgentContext]):
|
||||
name: str = "web_search_brave"
|
||||
@@ -528,7 +544,7 @@ class BraveWebSearchTool(FunctionTool[AstrAgentContext]):
|
||||
return _search_result_payload(results)
|
||||
|
||||
|
||||
@builtin_tool
|
||||
@builtin_tool(config=_BAIDU_WEB_SEARCH_TOOL_CONFIG)
|
||||
@pydantic_dataclass
|
||||
class BaiduWebSearchTool(FunctionTool[AstrAgentContext]):
|
||||
name: str = "web_search_baidu"
|
||||
|
||||
@@ -6,6 +6,7 @@ from astrbot.core import logger
|
||||
from astrbot.core.agent.mcp_client import MCPTool
|
||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||
from astrbot.core.star import star_map
|
||||
from astrbot.core.tools.registry import get_builtin_tool_config_statuses
|
||||
|
||||
from .route import Response, Route, RouteContext
|
||||
|
||||
@@ -434,13 +435,36 @@ class ToolsRoute(Route):
|
||||
if tool.name not in existing_names:
|
||||
tools.append(tool)
|
||||
|
||||
conf_list = self.core_lifecycle.astrbot_config_mgr.get_conf_list()
|
||||
conf_name_map = {conf["id"]: conf["name"] for conf in conf_list}
|
||||
config_entries = []
|
||||
for conf_id, conf in self.core_lifecycle.astrbot_config_mgr.confs.items():
|
||||
config_entries.append(
|
||||
{
|
||||
"conf_id": conf_id,
|
||||
"conf_name": conf_name_map.get(conf_id, conf_id),
|
||||
"config": conf,
|
||||
}
|
||||
)
|
||||
|
||||
tools_dict = []
|
||||
for tool in tools:
|
||||
readonly = False
|
||||
builtin_config_statuses = []
|
||||
builtin_config_tags = []
|
||||
if self.tool_mgr.is_builtin_tool(tool.name):
|
||||
origin = "builtin"
|
||||
origin_name = "AstrBot Core"
|
||||
readonly = True
|
||||
builtin_config_statuses = get_builtin_tool_config_statuses(
|
||||
tool.name,
|
||||
config_entries,
|
||||
)
|
||||
builtin_config_tags = [
|
||||
status
|
||||
for status in builtin_config_statuses
|
||||
if status["enabled"]
|
||||
]
|
||||
elif isinstance(tool, MCPTool):
|
||||
origin = "mcp"
|
||||
origin_name = tool.mcp_server_name
|
||||
@@ -462,6 +486,8 @@ class ToolsRoute(Route):
|
||||
"origin": origin,
|
||||
"origin_name": origin_name,
|
||||
"readonly": readonly,
|
||||
"builtin_config_statuses": builtin_config_statuses,
|
||||
"builtin_config_tags": builtin_config_tags,
|
||||
}
|
||||
tools_dict.append(tool_info)
|
||||
return Response().ok(data=tools_dict).__dict__
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import type { ToolItem } from '../types';
|
||||
import type { BuiltinToolConfigTag, ToolConfigCondition, ToolItem } from '../types';
|
||||
|
||||
const { tm: tmTool } = useModuleI18n('features/tooluse');
|
||||
|
||||
@@ -15,7 +15,7 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const toolHeaders = computed(() => [
|
||||
{ title: tmTool('functionTools.title'), key: 'name', minWidth: '240px' },
|
||||
{ title: tmTool('functionTools.title'), key: 'name', minWidth: '320px' },
|
||||
{ title: tmTool('functionTools.description'), key: 'description' },
|
||||
{ title: tmTool('functionTools.table.origin'), key: 'origin', sortable: false, width: '120px' },
|
||||
{ title: tmTool('functionTools.table.originName'), key: 'origin_name', sortable: false, width: '160px' },
|
||||
@@ -23,6 +23,52 @@ const toolHeaders = computed(() => [
|
||||
]);
|
||||
|
||||
const parameterEntries = (tool: ToolItem) => Object.entries(tool.parameters?.properties || {});
|
||||
|
||||
const formatConfigValue = (value: unknown) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(item => String(item)).join(', ');
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? 'true' : 'false';
|
||||
}
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '-';
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const formatCondition = (condition: ToolConfigCondition) => {
|
||||
if (condition.message) {
|
||||
return condition.message;
|
||||
}
|
||||
|
||||
switch (condition.operator) {
|
||||
case 'truthy':
|
||||
return tmTool('functionTools.configTags.conditions.truthy', {
|
||||
key: condition.key
|
||||
});
|
||||
case 'equals':
|
||||
return tmTool('functionTools.configTags.conditions.equals', {
|
||||
key: condition.key,
|
||||
expected: formatConfigValue(condition.expected)
|
||||
});
|
||||
case 'in':
|
||||
return tmTool('functionTools.configTags.conditions.in', {
|
||||
key: condition.key,
|
||||
expected: formatConfigValue(condition.expected)
|
||||
});
|
||||
default:
|
||||
return tmTool('functionTools.configTags.conditions.fallback', {
|
||||
key: condition.key,
|
||||
actual: formatConfigValue(condition.actual)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const enabledConfigTags = (tool: ToolItem): BuiltinToolConfigTag[] => {
|
||||
if (tool.origin !== 'builtin') return [];
|
||||
return (tool.builtin_config_tags || []).filter(tag => tag.enabled);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -38,7 +84,39 @@ const parameterEntries = (tool: ToolItem) => Object.entries(tool.parameters?.pro
|
||||
>
|
||||
<template #item.name="{ item }">
|
||||
<div class="py-2">
|
||||
<div class="tool-name text-body-2 font-weight-medium">{{ item.name }}</div>
|
||||
<div class="d-flex flex-wrap align-center ga-1">
|
||||
<div class="tool-name text-body-2 font-weight-medium">{{ item.name }}</div>
|
||||
<v-tooltip
|
||||
v-for="tag in enabledConfigTags(item)"
|
||||
:key="`${item.name}-${tag.conf_id}`"
|
||||
location="top"
|
||||
>
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-chip
|
||||
v-bind="tooltipProps"
|
||||
size="x-small"
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
class="text-caption font-weight-medium"
|
||||
>
|
||||
{{ tag.conf_name }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<div class="tool-config-tooltip">
|
||||
<div class="text-body-2 font-weight-medium mb-2">
|
||||
{{ tmTool('functionTools.configTags.tooltipTitle', { config: tag.conf_name }) }}
|
||||
</div>
|
||||
<div
|
||||
v-for="(condition, index) in tag.matched_conditions"
|
||||
:key="`${tag.conf_id}-${index}-${condition.key}`"
|
||||
class="text-body-2 text-medium-emphasis mb-1"
|
||||
>
|
||||
{{ formatCondition(condition) }}
|
||||
</div>
|
||||
</div>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -135,4 +213,16 @@ const parameterEntries = (tool: ToolItem) => Object.entries(tool.parameters?.pro
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.tool-config-tooltip {
|
||||
max-width: 360px;
|
||||
padding: 4px 0;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.tool-config-tooltip :deep(.text-body-2),
|
||||
.tool-config-tooltip :deep(.text-medium-emphasis),
|
||||
.tool-config-tooltip :deep(.font-weight-medium) {
|
||||
color: inherit !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -89,6 +89,23 @@ export interface ToolParameter {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ToolConfigCondition {
|
||||
key: string;
|
||||
operator: 'truthy' | 'equals' | 'in' | 'custom' | string;
|
||||
expected?: unknown;
|
||||
actual?: unknown;
|
||||
matched: boolean;
|
||||
message?: string | null;
|
||||
}
|
||||
|
||||
export interface BuiltinToolConfigTag {
|
||||
conf_id: string;
|
||||
conf_name: string;
|
||||
enabled: boolean;
|
||||
matched_conditions: ToolConfigCondition[];
|
||||
failed_conditions: ToolConfigCondition[];
|
||||
}
|
||||
|
||||
/** MCP/函数工具对象 */
|
||||
export interface ToolItem {
|
||||
name: string;
|
||||
@@ -100,4 +117,6 @@ export interface ToolItem {
|
||||
};
|
||||
origin?: string;
|
||||
origin_name?: string;
|
||||
builtin_config_statuses?: BuiltinToolConfigTag[];
|
||||
builtin_config_tags?: BuiltinToolConfigTag[];
|
||||
}
|
||||
|
||||
@@ -47,6 +47,15 @@
|
||||
"originName": "Origin Name",
|
||||
"readonly": "Read-only",
|
||||
"actions": "Actions"
|
||||
},
|
||||
"configTags": {
|
||||
"tooltipTitle": "This tool is enabled in config file {config} because:",
|
||||
"conditions": {
|
||||
"truthy": "{key} is enabled",
|
||||
"equals": "{key} = {expected}",
|
||||
"in": "{key} matched {expected}",
|
||||
"fallback": "{key} is currently {actual}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
|
||||
@@ -47,6 +47,15 @@
|
||||
"originName": "Имя источника",
|
||||
"readonly": "Только чтение",
|
||||
"actions": "Действия"
|
||||
},
|
||||
"configTags": {
|
||||
"tooltipTitle": "Этот инструмент включен в файле конфигурации {config}, потому что:",
|
||||
"conditions": {
|
||||
"truthy": "{key} включен",
|
||||
"equals": "{key} = {expected}",
|
||||
"in": "{key} соответствует {expected}",
|
||||
"fallback": "Текущее значение {key}: {actual}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
|
||||
@@ -47,6 +47,15 @@
|
||||
"originName": "来源名称",
|
||||
"readonly": "只读",
|
||||
"actions": "操作"
|
||||
},
|
||||
"configTags": {
|
||||
"tooltipTitle": "该工具在配置文件 {config} 中启用,因为:",
|
||||
"conditions": {
|
||||
"truthy": "启用了 {key}",
|
||||
"equals": "{key} = {expected}",
|
||||
"in": "{key} 命中了 {expected}",
|
||||
"fallback": "{key} 当前值为 {actual}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"marketplace": {
|
||||
|
||||
Reference in New Issue
Block a user