Toadied's picture
2312
8b383ad verified
Raw
History Blame Contribute Delete
10 kB
"""工具基类"""
from abc import ABC, abstractmethod
from typing import Dict, Any, List, Optional, Callable, get_type_hints
from pydantic import BaseModel
import inspect
import re
def tool_action(name: str = None, description: str = None):
"""装饰器:标记一个方法为可展开的工具 action
用法:
@tool_action("memory_add", "添加新记忆")
def _add_memory(self, content: str, importance: float = 0.5) -> str:
'''添加记忆
Args:
content: 记忆内容
importance: 重要性分数
'''
...
Args:
name: 工具名称(如果不提供,从方法名自动生成)
description: 工具描述(如果不提供,从 docstring 提取)
"""
def decorator(func: Callable):
func._is_tool_action = True
func._tool_name = name
func._tool_description = description
return func
return decorator
class ToolParameter(BaseModel):
"""工具参数定义"""
name: str
type: str
description: str
required: bool = True
default: Any = None
class Tool(ABC):
"""工具基类
支持两种使用模式:
1. 普通模式:工具作为单一实体使用
2. 可展开模式:工具可以展开为多个独立的子工具(每个子工具对应一个功能)
展开模式支持两种实现方式:
- 手动定义子工具类(传统方式)
- 使用 @tool_action 装饰器自动生成(推荐)
"""
def __init__(self, name: str, description: str, expandable: bool = False):
"""初始化工具
Args:
name: 工具名称
description: 工具描述
expandable: 是否可展开为多个子工具
"""
self.name = name
self.description = description
self.expandable = expandable
@abstractmethod
def run(self, parameters: Dict[str, Any]) -> str:
"""执行工具"""
pass
@abstractmethod
def get_parameters(self) -> List[ToolParameter]:
"""获取工具参数定义"""
pass
def get_expanded_tools(self) -> Optional[List['Tool']]:
"""获取展开后的子工具列表
默认实现:自动从标记了 @tool_action 的方法生成子工具
子类可以重写此方法提供自定义的展开逻辑
Returns:
如果工具支持展开,返回子工具列表;否则返回 None
"""
if not self.expandable:
return None
# 自动从装饰器标记的方法生成工具
tools = []
for name, method in inspect.getmembers(self, predicate=inspect.ismethod):
if hasattr(method, '_is_tool_action'):
tool = AutoGeneratedTool(
parent=self,
method=method,
name=method._tool_name,
description=method._tool_description
)
tools.append(tool)
return tools if tools else None
def validate_parameters(self, parameters: Dict[str, Any]) -> bool:
"""验证参数"""
required_params = [p.name for p in self.get_parameters() if p.required]
return all(param in parameters for param in required_params)
def to_dict(self) -> Dict[str, Any]:
"""转换为字典格式"""
return {
"name": self.name,
"description": self.description,
"parameters": [param.dict() for param in self.get_parameters()]
}
def to_openai_schema(self) -> Dict[str, Any]:
"""转换为 OpenAI function calling schema 格式
用于 FunctionCallAgent,使工具能够被 OpenAI 原生 function calling 使用
Returns:
符合 OpenAI function calling 标准的 schema
"""
parameters = self.get_parameters()
# 构建 properties
properties = {}
required = []
for param in parameters:
# 基础属性定义
prop = {
"type": param.type,
"description": param.description
}
# 如果有默认值,添加到描述中(OpenAI schema 不支持 default 字段)
if param.default is not None:
prop["description"] = f"{param.description} (默认: {param.default})"
# 如果是数组类型,添加 items 定义
if param.type == "array":
prop["items"] = {"type": "string"} # 默认字符串数组
properties[param.name] = prop
# 收集必需参数
if param.required:
required.append(param.name)
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": {
"type": "object",
"properties": properties,
"required": required
}
}
}
def __str__(self) -> str:
return f"Tool(name={self.name})"
def __repr__(self) -> str:
return self.__str__()
class AutoGeneratedTool(Tool):
"""自动生成的工具 - 从方法签名和 docstring 自动提取参数"""
def __init__(self, parent: Tool, method: Callable, name: str = None, description: str = None):
"""初始化自动生成的工具
Args:
parent: 父工具实例
method: 被装饰的方法
name: 工具名称(如果为 None,从方法名生成)
description: 工具描述(如果为 None,从 docstring 提取)
"""
self.parent = parent
self.method = method
# 生成工具名称
if name is None:
# 从方法名生成:_add_memory -> parent_name_add_memory
method_name = method.__name__.lstrip('_')
name = f"{parent.name}_{method_name}"
# 提取描述
if description is None:
description = self._extract_description_from_docstring()
super().__init__(name=name, description=description)
# 自动解析参数
self._parameters = self._parse_parameters()
def _extract_description_from_docstring(self) -> str:
"""从 docstring 提取描述"""
doc = inspect.getdoc(self.method)
if not doc:
return f"执行 {self.method.__name__}"
# 提取第一行作为描述
lines = doc.split('\n')
for line in lines:
line = line.strip()
if line and not line.startswith('Args:') and not line.startswith('Returns:'):
return line
return f"执行 {self.method.__name__}"
def _parse_parameters(self) -> List[ToolParameter]:
"""从方法签名和 docstring 自动提取参数"""
sig = inspect.signature(self.method)
type_hints = get_type_hints(self.method)
docstring = inspect.getdoc(self.method) or ""
# 从 docstring 解析参数描述
param_descriptions = self._parse_param_descriptions(docstring)
parameters = []
for param_name, param in sig.parameters.items():
if param_name == 'self':
continue
# 获取类型
param_type_hint = type_hints.get(param_name, str)
param_type = self._python_type_to_tool_type(param_type_hint)
# 判断是否必需
required = param.default == inspect.Parameter.empty
default = None if required else param.default
# 获取描述
description = param_descriptions.get(param_name, f"参数 {param_name}")
parameters.append(ToolParameter(
name=param_name,
type=param_type,
description=description,
required=required,
default=default
))
return parameters
def _parse_param_descriptions(self, docstring: str) -> Dict[str, str]:
"""从 docstring 解析参数描述
支持格式:
Args:
param_name: 参数描述
another_param: 另一个参数描述
"""
descriptions = {}
# 查找 Args: 部分
args_match = re.search(r'Args:\s*\n(.*?)(?:\n\s*\n|Returns:|$)', docstring, re.DOTALL)
if not args_match:
return descriptions
args_section = args_match.group(1)
# 解析每个参数
# 匹配格式: param_name: 描述 或 param_name (type): 描述
param_pattern = r'^\s*(\w+)(?:\s*\([^)]+\))?\s*:\s*(.+?)(?=^\s*\w+\s*(?:\([^)]+\))?\s*:|$)'
matches = re.finditer(param_pattern, args_section, re.MULTILINE | re.DOTALL)
for match in matches:
param_name = match.group(1).strip()
param_desc = match.group(2).strip()
# 清理描述中的多余空白
param_desc = re.sub(r'\s+', ' ', param_desc)
descriptions[param_name] = param_desc
return descriptions
def _python_type_to_tool_type(self, py_type) -> str:
"""将 Python 类型转换为工具类型字符串"""
# 处理泛型类型
origin = getattr(py_type, '__origin__', None)
if origin is not None:
if origin is list:
return "array"
elif origin is dict:
return "object"
# 处理基本类型
type_map = {
str: "string",
int: "integer",
float: "number",
bool: "boolean",
list: "array",
dict: "object",
}
return type_map.get(py_type, "string")
def get_parameters(self) -> List[ToolParameter]:
"""获取参数列表"""
return self._parameters
def run(self, parameters: Dict[str, Any]) -> str:
"""执行方法"""
return self.method(**parameters)