Files
2026-07-06 09:16:25 +08:00

359 lines
13 KiB
Python

import inspect
import json
import functools
import os
from typing import Dict, Any, Callable, List, Optional, get_type_hints, Union
class ToolRegistry:
# 状态持久化文件路径
STATE_FILE = "/www/server/panel/data/agent/tools_state.json"
def __init__(self):
self._tools: Dict[str, Callable] = {}
self._schemas: List[Dict[str, Any]] = []
self._metadata: Dict[str, Dict[str, Any]] = {}
self._states: Dict[str, Dict[str, Any]] = self._load_states()
def _load_states(self) -> Dict[str, Any]:
"""从文件加载工具状态"""
if os.path.exists(self.STATE_FILE):
try:
with open(self.STATE_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except:
pass
return {}
def _save_states(self):
"""将工具状态保存到文件"""
try:
with open(self.STATE_FILE, 'w', encoding='utf-8') as f:
json.dump(self._states, f, indent=4, ensure_ascii=False)
except:
pass
def tool_exists(self, name: str) -> bool:
"""检查工具是否存在"""
return name in self._tools
def is_tool_enabled(self, name: str, enabled_ids: List[str]) -> bool:
"""
检查工具是否在允许列表中。
Args:
name: 工具名称(函数名)
enabled_ids: 允许使用的工具ID列表
Returns:
bool: 工具是否被允许使用
"""
meta = self._metadata.get(name)
if not meta:
return False
return meta["id"] in enabled_ids
def get_tool_id(self, name: str) -> Optional[str]:
"""获取工具的ID"""
meta = self._metadata.get(name)
if meta:
return meta["id"]
return None
def register_tool(self, tool_id: Union[str, Callable, type] = None, **kwargs):
"""
注册工具的装饰器。
支持:
@register_tool
@register_tool("my_tool_id")
@register_tool(id="my_tool_id")
@register_tool(id="my_tool_id", category="system")
@register_tool(id="my_tool_id", category="system", name_cn="系统服务")
以及装饰类:
@register_tool
class MyTool:
def execute(self, ...): ...
"""
category = kwargs.get("category", "default")
name_cn = kwargs.get("name_cn", "")
risk_level = kwargs.get("risk_level", "low")
subagent_only = kwargs.get("subagent_only", False)
internal = kwargs.get("internal", False)
# 处理 id="xxx" 的关键字参数情况
if tool_id is None and "id" in kwargs:
tool_id = kwargs["id"]
# 如果是类 (作为装饰器无参数直接使用 @register_tool)
if inspect.isclass(tool_id):
return self._register_class(tool_id, None, category, name_cn, risk_level, subagent_only, internal)
# 如果是函数 (作为普通装饰器使用 @register_tool (无参数))
if callable(tool_id):
func = tool_id
return self._register_func(func, None, category, name_cn, risk_level, subagent_only, internal)
# 如果带有参数 @register_tool(...)
def decorator(obj):
if inspect.isclass(obj):
return self._register_class(obj, tool_id, category, name_cn, risk_level, subagent_only, internal)
else:
return self._register_func(obj, tool_id, category, name_cn, risk_level, subagent_only, internal)
return decorator
def _register_class(self, clazz: type, tool_id: Optional[str], category: str, name_cn: str, risk_level: str, subagent_only: bool = False, internal: bool = False):
# 实例化类
try:
instance = clazz()
except Exception as e:
raise ValueError(f"Failed to instantiate tool class {clazz.__name__}: {e}")
# 查找入口方法
func = None
if hasattr(instance, 'execute') and callable(instance.execute):
func = instance.execute
elif callable(instance):
func = instance.__call__
else:
raise ValueError(f"Class {clazz.__name__} must implement 'execute' method or be callable.")
# 确定工具ID
if not tool_id:
tool_id = clazz.__name__
# 创建包装器以保持正确的名称和文档
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
wrapper.__name__ = tool_id
# 如果方法没有文档字符串,尝试使用类的文档字符串
if not wrapper.__doc__:
wrapper.__doc__ = inspect.getdoc(clazz)
# 注册包装后的函数
# 注意:这里我们返回 clazz,以便类定义保持不变,
# 但我们在内部注册了 wrapper 函数作为工具执行体。
self._register_func(wrapper, tool_id, category, name_cn, risk_level, subagent_only, internal)
return clazz
def _register_func(self, func: Callable, tool_id: Optional[str], category: str, name_cn: str, risk_level: str, subagent_only: bool = False, internal: bool = False):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
name = func.__name__
# 如果没有提供ID,使用函数名作为ID
final_id = tool_id if tool_id else name
self._tools[name] = func
schema = self._generate_schema(func)
self._schemas.append(schema)
# 存储元数据
state = self._states.get(final_id, {"show": True})
self._metadata[name] = {
"id": final_id,
"name": name,
"name_cn": name_cn,
"category": category,
"risk_level": risk_level,
"description": schema["function"]["description"],
"show": state.get("show", True),
"subagent_only": subagent_only,
"internal": internal
}
return wrapper
def set_tool_show_status(self, tool_id: str = None, show: bool = True, category: str = None) -> bool:
"""设置工具或分类下工具的显示状态"""
found = False
# 如果提供了 category,按分类批量设置
if category:
for name, meta in self._metadata.items():
if meta.get("category") == category:
meta["show"] = show
self._states[meta["id"]] = {"show": show}
found = True
# 如果提供了 tool_id,按 ID 设置
elif tool_id:
for name, meta in self._metadata.items():
if meta["id"] == tool_id:
meta["show"] = show
self._states[tool_id] = {"show": show}
found = True
if found:
self._save_states()
return True
return False
def get_openai_tools(self, enabled_ids: List[str] = None) -> List[Dict[str, Any]]:
"""
返回 OpenAI 格式的工具定义。
Args:
enabled_ids: 允许使用的工具ID列表。如果不传,则返回所有(兼容旧行为)。
"""
# 动态更新支持动态文档的工具 schema
self._refresh_dynamic_docs()
if enabled_ids is None:
return self._schemas
filtered_schemas = []
for schema in self._schemas:
name = schema["function"]["name"]
meta = self._metadata.get(name)
# 只有 ID 在启用列表中才返回
if meta and meta["id"] in enabled_ids:
filtered_schemas.append(schema)
return filtered_schemas
def _refresh_dynamic_docs(self):
"""
刷新支持动态文档的工具的 schema description。
对于实现了动态 __doc__ property 的工具,每次获取工具列表时重新生成 description。
"""
for name, func in self._tools.items():
# 获取实际的文档字符串(会调用 __doc__ property 如果存在)
doc = inspect.getdoc(func)
if doc and doc != "No description provided.":
# 更新 schema 中的 description
for schema in self._schemas:
if schema["function"]["name"] == name:
if schema["function"]["description"] != doc:
schema["function"]["description"] = doc
break
def get_internal_tools(self) -> List[str]:
"""internal=True 的工具名: 强开启(无条件 enabled) + STEALTH(前端不显示)"""
return [
name for name, meta in self._metadata.items() if meta.get("internal")
]
def get_all_tools_info(self) -> List[Dict[str, Any]]:
"""获取所有工具的详细信息列表 (用于前端展示, 去重)"""
infos = []
seen_ids = set()
for name, meta in self._metadata.items():
tool_id = meta.get("id", name)
if tool_id in seen_ids:
continue
seen_ids.add(tool_id)
# 创建副本以免修改原始元数据
info = meta.copy()
# 如果存在 name_cn,替换 name 字段
if info.get("name_cn"):
info["name"] = info["name_cn"]
infos.append(info)
return infos
def get_tool_func(self, name: str) -> Optional[Callable]:
return self._tools.get(name)
def _generate_schema(self, func: Callable) -> Dict[str, Any]:
"""根据文档字符串和类型提示生成 OpenAI 函数 Schema"""
target_func = func
if not inspect.isfunction(func) and not inspect.ismethod(func):
if hasattr(func, '__call__'):
target_func = func.__call__
sig = inspect.signature(target_func)
doc = inspect.getdoc(func) or "No description provided."
parameters = {
"type": "object",
"properties": {},
"required": [],
"additionalProperties": False
}
try:
type_hints = get_type_hints(target_func)
except Exception:
type_hints = {}
for name, param in sig.parameters.items():
if name == "self" or name == "cls":
continue
# Skip *args and **kwargs
if param.kind == inspect.Parameter.VAR_POSITIONAL or param.kind == inspect.Parameter.VAR_KEYWORD:
continue
# Resolve type: try get_type_hints, fall back to param.annotation
param_type = type_hints.get(name) if type_hints and name in type_hints else param.annotation
if param_type is inspect.Parameter.empty:
param_type = str
try:
param_schema = self._python_type_to_json_schema(param_type)
except Exception:
param_schema = {"type": "string"}
parameters["properties"][name] = param_schema
# 无默认值的参数为必填, 有默认值的参数为可选
if param.default is inspect.Parameter.empty:
parameters["required"].append(name)
return {
"type": "function",
"function": {
"name": func.__name__,
"strict": False,
"description": doc,
"parameters": parameters
}
}
def _python_type_to_json_schema(self, py_type) -> Dict[str, Any]:
"""
将 Python 类型转换为 JSON Schema 格式
对于数组类型,会自动添加 items 字段
"""
if py_type == int:
return {"type": "integer"}
elif py_type == float:
return {"type": "number"}
elif py_type == bool:
return {"type": "boolean"}
elif py_type == list or getattr(py_type, "__origin__", None) == list:
# 数组类型需要指定 items
# 尝试获取元素类型 (List[str], List[int] 等)
element_schema = {"type": "string"} # 默认元素类型为 string
if hasattr(py_type, "__args__") and py_type.__args__:
arg_type = py_type.__args__[0]
element_schema = self._python_type_to_json_schema(arg_type)
return {"type": "array", "items": element_schema}
elif py_type == dict or getattr(py_type, "__origin__", None) == dict:
return {"type": "object", "additionalProperties": False}
else:
return {"type": "string"}
# 全局注册实例
registry = ToolRegistry()
# 装饰器别名
def register_tool(tool_id=None, **kwargs):
return registry.register_tool(tool_id, **kwargs)
# 导入所有工具以确保它们被注册
from . import search
from . import edit
from . import terminal
from . import readonly_command
from . import task
from . import summary
from . import webfetch
from . import skill
from . import read
from . import todo
from . import panel_tools
from . import mysql_tools
from . import panel_docs
from . import memory